Skip to main content

connect_1password/models/
item.rs

1use std::ascii::AsciiExt;
2
3use crate::error::{CustomError, Error};
4use chrono::{DateTime, Utc};
5use hyper::StatusCode;
6use log::debug;
7use regex::Regex;
8use serde::{Deserialize, Serialize};
9use uuid::Uuid;
10
11/// Defines an Item Object
12#[derive(Debug, Deserialize, PartialEq)]
13pub struct ItemData {
14    /// The UUID of the item.
15    pub id: String,
16    /// The title of the item.
17    pub title: String,
18    /// An object containing an id property whose value is the UUID of the vault the item is in.
19    pub vault: VaultID,
20    /// The category of the item.
21    pub category: Option<String>,
22    /// Vector of URL objects containing URLs for the item.
23    pub urls: Option<Vec<UrlObject>>,
24    /// Whether the item is marked as a favourite.
25    pub favorite: Option<bool>,
26    /// A vector of strings of the tags assigned to the item.
27    pub tags: Option<Vec<String>>,
28    /// The state of the item.
29    pub state: Option<String>,
30    /// Date and time when the vault was created.
31    pub created_at: Option<DateTime<Utc>>,
32    /// Date and time when the vault or its contents were last changed.
33    pub updated_at: Option<DateTime<Utc>>,
34}
35
36/// Defines the Vault UUID via a record struct
37#[derive(Debug, Deserialize, Serialize, PartialEq, Clone)]
38pub struct VaultID {
39    /// The UUID of the vault.
40    pub id: String,
41}
42
43/// Defines a URL Object
44#[derive(Debug, Deserialize, Serialize, PartialEq, Clone)]
45pub struct UrlObject {
46    /// The address.
47    pub url: String,
48    /// Whether this is the primary URL for the item.
49    pub primary: bool,
50}
51
52/// This is a Field Object
53#[derive(Debug, Deserialize, Serialize, Clone)]
54pub struct FieldObject {
55    /// An object containing the UUID of a section in the item.
56    pub section: Option<SectionID>,
57    /// Use `purpose` for the username, password, and notes fields.
58    pub purpose: Option<String>,
59    /// Use `type' for all other fields
60    pub r#type: Option<String>,
61    /// The value to save for the field. You can specify a `generate` field instead of `value` to create a password or other random information for the value.
62    pub value: Option<String>,
63    /// Generate a password and save in the value for the field. By default, the password is a 32-characters long, made up of letters, numbers, and symbols. To customize the password, include a `recipe` field.
64    pub generate: Option<bool>,
65    // FIXME the GeneratorRecipe needs to be added
66    // pub recipe
67    /// Some optional text
68    pub label: Option<String>,
69}
70
71/// Used to specify type of a Field Object
72#[derive(Debug)]
73pub enum FieldType {
74    /// Item value will be concealed
75    Concealed,
76}
77
78impl Into<String> for FieldType {
79    fn into(self) -> String {
80        let value = match self {
81            Self::Concealed => "CONCEALED",
82        };
83
84        value.to_string()
85    }
86}
87
88/// This is a Section Object
89#[derive(Debug, Deserialize, Serialize, Clone)]
90pub struct SectionObject {
91    /// The UUID of the section.
92    pub id: String,
93    /// Some optional text
94    pub label: Option<String>,
95}
96
97impl SectionObject {
98    /// Create a new instance
99    pub fn new(id: &str, label: &str) -> Self {
100        Self {
101            id: id.to_string(),
102            label: Some(label.to_string()),
103        }
104    }
105}
106
107/// This is a SectionID
108#[derive(Debug, Deserialize, Serialize, Clone)]
109pub struct SectionID {
110    /// The UUID of the section.
111    pub id: String,
112}
113
114impl SectionID {
115    /// Create new instance
116    pub fn new() -> Self {
117        Self {
118            id: Uuid::new_v4().to_string(),
119        }
120    }
121}
122
123/// This is a FullItem
124#[derive(Debug, Deserialize, Serialize)]
125pub struct FullItem {
126    /// The title of the item.
127    pub title: String,
128    /// An object containing an id property whose value is the UUID of the vault the item is in.
129    pub vault: VaultID,
130    /// The category of the item.
131    pub category: Option<String>,
132    /// Vector of URL objects containing URLs for the item.
133    pub urls: Option<Vec<UrlObject>>,
134    /// Whether the item is marked as a favourite.
135    pub favorite: Option<bool>,
136    /// A vector of strings of the tags assigned to the item.
137    pub tags: Option<Vec<String>>,
138    /// A vector of Field objects of the fields to include with the item.
139    pub fields: Vec<FieldObject>,
140    /// A vector of Section objects of the sections to include with the item.
141    pub sections: Vec<SectionObject>,
142}
143
144/// Defines a default interface
145pub trait DefaultItem {
146    /// Execute the builder
147    fn build(&self) -> Result<FullItem, Box<dyn std::error::Error + Send + Sync>>;
148}
149
150/// Defines an interface for a Login item
151pub trait LoginItem {
152    /// Specify title
153    fn title(self, username: &str) -> Self;
154    /// Specify username
155    fn username(self, username: &str) -> Self;
156    /// Specify password
157    fn password(self, password: &str) -> Self;
158    /// Execute the builder
159    fn build(&self) -> Result<FullItem, Box<dyn std::error::Error + Send + Sync>>;
160}
161
162/// Defines an interface for a Api Credential item
163pub trait ApiCredentialItem {
164    /// Specify API key
165    fn api_key(self, key: &str, title: &str) -> Self;
166    /// Execute the builder
167    fn build(&self) -> Result<FullItem, Box<dyn std::error::Error + Send + Sync>>;
168}
169
170/// This is an ItemBuilder
171#[derive(Debug)]
172pub struct ItemBuilder {
173    /// The title of the item.
174    pub title: String,
175    /// An object containing an id property whose value is the UUID of the vault the item is in.
176    pub vault: VaultID,
177    /// The category of the item.
178    pub category: Option<String>,
179    /// Vector of URL objects containing URLs for the item.
180    pub urls: Option<Vec<UrlObject>>,
181    /// Whether the item is marked as a favourite.
182    pub favorite: Option<bool>,
183    /// A vector of strings of the tags assigned to the item.
184    pub tags: Option<Vec<String>>,
185    /// A vector of Field objects of the fields to include with the item.
186    pub fields: Vec<FieldObject>,
187    /// A vector of Section objects of the sections to include with the item.
188    pub sections: Vec<SectionObject>,
189}
190
191/// Describes usable Item categories
192#[derive(Debug)]
193pub enum ItemCategory {
194    /// API Credential
195    ApiCredential,
196    /// Login
197    Login,
198    /// Password
199    Password,
200}
201
202impl ItemCategory {
203    fn default() -> Self {
204        Self::ApiCredential
205    }
206}
207
208impl Into<String> for ItemCategory {
209    fn into(self) -> String {
210        let value = match self {
211            Self::ApiCredential => "API_CREDENTIAL",
212            Self::Login => "LOGIN",
213            Self::Password => "PASSWORD",
214        };
215
216        value.to_string()
217    }
218}
219
220impl ItemBuilder {
221    /// Create a new instance
222    pub fn new(vault_id: &str, category: ItemCategory) -> Self {
223        let vault = VaultID {
224            id: vault_id.to_string(),
225        };
226
227        Self {
228            vault,
229            title: String::default(),
230            category: Some(category.into()),
231            favorite: Some(false),
232            urls: None,
233            tags: None,
234            fields: vec![],
235            sections: vec![],
236        }
237    }
238
239    // FIXME: This needs testing to ensure the OTP secret is applied correctly
240    pub(crate) fn add_otp(mut self, secret: &str) -> Self {
241        let section = SectionID::new();
242        let section_obj = SectionObject::new(&section.id, "OTP");
243
244        self.sections.push(section_obj);
245
246        let field_object = FieldObject {
247            section: Some(section),
248            label: None,
249            purpose: None,
250            r#type: Some("OTP".to_string()),
251            generate: Some(true),
252            value: Some(secret.to_string()),
253        };
254        self.fields.push(field_object);
255
256        self
257    }
258}
259
260impl DefaultItem for ItemBuilder {
261    fn build(&self) -> Result<FullItem, Box<dyn std::error::Error + Send + Sync>> {
262        Ok(FullItem {
263            title: self.title.clone(),
264            category: self.category.clone(),
265            favorite: self.favorite,
266            fields: self.fields.clone(),
267            sections: self.sections.clone(),
268            tags: self.tags.clone(),
269            urls: self.urls.clone(),
270            vault: self.vault.clone(),
271        })
272    }
273}
274
275impl LoginItem for ItemBuilder {
276    fn title(mut self, title: &str) -> Self {
277        self.title = title.to_string();
278        self
279    }
280
281    fn username(mut self, username: &str) -> Self {
282        let field: FieldObject = FieldObject {
283            value: Some(username.to_string()),
284            purpose: Some("USERNAME".to_string()),
285            generate: None,
286            label: None,
287            r#type: None,
288            section: None,
289        };
290
291        self.fields.push(field);
292        self
293    }
294
295    fn password(mut self, password: &str) -> Self {
296        let field: FieldObject = FieldObject {
297            value: password.is_empty().then(|| password.to_string()),
298            purpose: Some("PASSWORD".to_string()),
299            generate: password.is_empty().then(|| true),
300            label: None,
301            r#type: None,
302            section: None,
303        };
304
305        self.fields.push(field);
306        self
307    }
308
309    fn build(&self) -> Result<FullItem, Box<dyn std::error::Error + Send + Sync>> {
310        if self.title.is_empty() {
311            return Err(Box::new(CustomError::new("Title is required")));
312        }
313
314        Ok(FullItem {
315            title: self.title.clone(),
316            category: self.category.clone(),
317            favorite: self.favorite,
318            fields: self.fields.clone(),
319            sections: self.sections.clone(),
320            tags: self.tags.clone(),
321            urls: self.urls.clone(),
322            vault: self.vault.clone(),
323        })
324    }
325}
326
327impl ApiCredentialItem for ItemBuilder {
328    fn api_key(mut self, key: &str, title: &str) -> Self {
329        let section = SectionID::new();
330        let section_obj = SectionObject::new(&section.id, "API Key");
331
332        self.sections.push(section_obj);
333
334        let field_object = FieldObject {
335            section: Some(section),
336            label: None,
337            purpose: None,
338            r#type: Some(FieldType::Concealed.into()),
339            generate: Some(key.is_empty()),
340            value: Some(key.to_string()),
341        };
342        self.fields.push(field_object);
343        self.title = title.to_string();
344
345        self
346    }
347
348    fn build(&self) -> Result<FullItem, Box<dyn std::error::Error + Send + Sync>> {
349        Ok(FullItem {
350            title: self.title.clone(),
351            category: self.category.clone(),
352            favorite: self.favorite,
353            fields: self.fields.clone(),
354            sections: self.sections.clone(),
355            tags: self.tags.clone(),
356            urls: self.urls.clone(),
357            vault: self.vault.clone(),
358        })
359    }
360}