sos_database/entity/
folder.rs

1use crate::{Error, Result};
2use async_sqlite::rusqlite::{
3    CachedStatement, Connection, Error as SqlError, OptionalExtension, Row,
4    Transaction,
5};
6use async_sqlite::Client;
7use sos_core::crypto::Seed;
8use sos_core::{
9    commit::CommitHash, crypto::AeadPack, decode, encode, SecretId,
10    UtcDateTime, VaultCommit, VaultEntry, VaultFlags, VaultId,
11};
12use sos_vault::{SharedAccess, Summary, Vault};
13use sql_query_builder as sql;
14use std::collections::HashMap;
15use std::ops::Deref;
16use std::result::Result as StdResult;
17
18fn folder_select_columns(sql: sql::Select) -> sql::Select {
19    sql.select(
20        r#"
21            folders.folder_id,
22            folders.created_at,
23            folders.modified_at,
24            folders.identifier,
25            folders.name,
26            folders.salt,
27            folders.meta,
28            folders.seed,
29            folders.version,
30            folders.cipher,
31            folders.kdf,
32            folders.flags,
33            folders.shared_access
34        "#,
35    )
36}
37
38fn secret_select_columns(sql: sql::Select) -> sql::Select {
39    sql.select(
40        r#"
41            secret_id,
42            created_at,
43            modified_at,
44            identifier,
45            commit_hash,
46            meta,
47            secret 
48        "#,
49    )
50}
51
52/// Folder row from the database.
53#[doc(hidden)]
54#[derive(Debug, Default)]
55pub struct FolderRow {
56    pub row_id: i64,
57    created_at: String,
58    modified_at: String,
59    identifier: String,
60    name: String,
61    salt: Option<String>,
62    meta: Option<Vec<u8>>,
63    seed: Option<Vec<u8>>,
64    version: i64,
65    cipher: String,
66    kdf: String,
67    flags: Vec<u8>,
68    shared_access: Option<Vec<u8>>,
69}
70
71impl FolderRow {
72    /// Create a new folder row to insert.
73    pub async fn new_insert(vault: &Vault) -> Result<Self> {
74        let meta = if let Some(meta) = vault.header().meta() {
75            Some(encode(meta).await?)
76        } else {
77            None
78        };
79        let salt = vault.salt().cloned();
80        let seed = vault.seed().map(|s| s.as_ref().to_vec());
81        Self::new_insert_parts(vault.summary(), salt, meta, seed, None)
82    }
83
84    /// Create a new folder row to be inserted from parts.
85    pub fn new_insert_parts(
86        summary: &Summary,
87        salt: Option<String>,
88        meta: Option<Vec<u8>>,
89        seed: Option<Vec<u8>>,
90        shared_access: Option<Vec<u8>>,
91    ) -> Result<Self> {
92        Ok(Self {
93            created_at: UtcDateTime::default().to_rfc3339()?,
94            modified_at: UtcDateTime::default().to_rfc3339()?,
95            identifier: summary.id().to_string(),
96            name: summary.name().to_string(),
97            salt,
98            meta,
99            seed,
100            version: *summary.version() as i64,
101            cipher: summary.cipher().to_string(),
102            kdf: summary.kdf().to_string(),
103            flags: summary.flags().bits().to_le_bytes().to_vec(),
104            shared_access,
105            ..Default::default()
106        })
107    }
108
109    /// Create a new folder row to update.
110    pub async fn new_update(vault: &Vault) -> Result<Self> {
111        let summary = vault.summary();
112        let meta = if let Some(meta) = vault.header().meta() {
113            Some(encode(meta).await?)
114        } else {
115            None
116        };
117        let salt = vault.salt().cloned();
118        let seed = vault.seed().map(|s| s.as_ref().to_vec());
119        Ok(Self {
120            modified_at: UtcDateTime::default().to_rfc3339()?,
121            identifier: summary.id().to_string(),
122            name: summary.name().to_string(),
123            salt,
124            meta,
125            seed,
126            version: *summary.version() as i64,
127            cipher: summary.cipher().to_string(),
128            kdf: summary.kdf().to_string(),
129            flags: summary.flags().bits().to_le_bytes().to_vec(),
130            ..Default::default()
131        })
132    }
133}
134
135impl<'a> TryFrom<&Row<'a>> for FolderRow {
136    type Error = SqlError;
137    fn try_from(row: &Row<'a>) -> StdResult<Self, Self::Error> {
138        Ok(FolderRow {
139            row_id: row.get(0)?,
140            created_at: row.get(1)?,
141            modified_at: row.get(2)?,
142            identifier: row.get(3)?,
143            name: row.get(4)?,
144            salt: row.get(5)?,
145            meta: row.get(6)?,
146            seed: row.get(7)?,
147            version: row.get(8)?,
148            cipher: row.get(9)?,
149            kdf: row.get(10)?,
150            flags: row.get(11)?,
151            shared_access: row.get(12)?,
152        })
153    }
154}
155
156/// Folder record from the database.
157#[derive(Debug, Clone)]
158pub struct FolderRecord {
159    /// Row identifier.
160    pub row_id: i64,
161    /// Created date and time.
162    pub created_at: UtcDateTime,
163    /// Modified date and time.
164    pub modified_at: UtcDateTime,
165    /// Key derivation salt.
166    pub salt: Option<String>,
167    /// Folder meta data.
168    pub meta: Option<AeadPack>,
169    /// Optional seed entropy.
170    pub seed: Option<Seed>,
171    /// Folder summary.
172    pub summary: Summary,
173    /// Shared access permissions.
174    pub shared_access: Option<SharedAccess>,
175}
176
177impl FolderRecord {
178    /// Convert from a folder row.
179    pub async fn from_row(value: FolderRow) -> Result<Self> {
180        let created_at = UtcDateTime::parse_rfc3339(&value.created_at)?;
181        let modified_at = UtcDateTime::parse_rfc3339(&value.modified_at)?;
182        let folder_id: VaultId = value.identifier.parse()?;
183        let version: u16 = value.version.try_into()?;
184        let cipher = value.cipher.parse()?;
185        let kdf = value.kdf.parse()?;
186        let bytes: [u8; 8] = value.flags.as_slice().try_into()?;
187        let bits = u64::from_le_bytes(bytes);
188        let flags = VaultFlags::from_bits(bits)
189            .ok_or(sos_vault::Error::InvalidVaultFlags)?;
190
191        let salt = value.salt;
192
193        let meta = if let Some(meta) = &value.meta {
194            Some(decode(meta).await?)
195        } else {
196            None
197        };
198
199        let seed = if let Some(seed) = value.seed {
200            let seed: [u8; Seed::SIZE] = seed.as_slice().try_into()?;
201            Some(Seed(seed))
202        } else {
203            None
204        };
205
206        let summary =
207            Summary::new(version, folder_id, value.name, cipher, kdf, flags);
208
209        let shared_access = if let Some(shared_access) = &value.shared_access
210        {
211            Some(decode(shared_access).await?)
212        } else {
213            None
214        };
215
216        Ok(FolderRecord {
217            row_id: value.row_id,
218            created_at,
219            modified_at,
220            salt,
221            meta,
222            seed,
223            summary,
224            shared_access,
225        })
226    }
227
228    /// Convert a folder record into a vault.
229    pub fn into_vault(&self) -> Result<Vault> {
230        let mut vault: Vault = self.summary.clone().into();
231        vault.header_mut().set_meta(self.meta.clone());
232        vault.header_mut().set_salt(self.salt.clone());
233        vault.header_mut().set_seed(self.seed);
234        if let Some(shared_access) = &self.shared_access {
235            vault.header_mut().set_shared_access(shared_access.clone());
236        }
237        Ok(vault)
238    }
239}
240
241/// Secret row from the database.
242#[doc(hidden)]
243#[derive(Debug, Default)]
244pub struct SecretRow {
245    pub row_id: i64,
246    created_at: String,
247    modified_at: String,
248    identifier: String,
249    commit: Vec<u8>,
250    meta: Vec<u8>,
251    secret: Vec<u8>,
252}
253
254impl SecretRow {
255    /// Create a new secret row for insertion.
256    pub async fn new(
257        secret_id: &SecretId,
258        commit: &CommitHash,
259        entry: &VaultEntry,
260    ) -> Result<Self> {
261        let VaultEntry(meta, secret) = entry;
262        let meta = encode(meta).await?;
263        let secret = encode(secret).await?;
264        Ok(Self {
265            created_at: UtcDateTime::default().to_rfc3339()?,
266            modified_at: UtcDateTime::default().to_rfc3339()?,
267            identifier: secret_id.to_string(),
268            commit: commit.as_ref().to_vec(),
269            meta,
270            secret,
271            ..Default::default()
272        })
273    }
274
275    /// Secret identifier.
276    pub fn identifier(&self) -> &str {
277        &self.identifier
278    }
279
280    /// Commit hash.
281    pub fn commit(&self) -> &[u8] {
282        &self.commit
283    }
284
285    /// Meta data bytes.
286    pub fn meta_bytes(&self) -> &[u8] {
287        &self.meta
288    }
289
290    /// Secret data bytes.
291    pub fn secret_bytes(&self) -> &[u8] {
292        &self.secret
293    }
294}
295
296impl<'a> TryFrom<&Row<'a>> for SecretRow {
297    type Error = SqlError;
298    fn try_from(row: &Row<'a>) -> StdResult<Self, Self::Error> {
299        Ok(SecretRow {
300            row_id: row.get(0)?,
301            created_at: row.get(1)?,
302            modified_at: row.get(2)?,
303            identifier: row.get(3)?,
304            commit: row.get(4)?,
305            meta: row.get(5)?,
306            secret: row.get(6)?,
307        })
308    }
309}
310
311/// Secret record from the database.
312#[doc(hidden)]
313#[derive(Debug)]
314pub struct SecretRecord {
315    pub row_id: i64,
316    pub created_at: UtcDateTime,
317    pub modified_at: UtcDateTime,
318    pub secret_id: VaultId,
319    pub commit: VaultCommit,
320}
321
322impl SecretRecord {
323    /// Convert from a secret row.
324    pub async fn from_row(value: SecretRow) -> Result<Self> {
325        let created_at = UtcDateTime::parse_rfc3339(&value.created_at)?;
326        let modified_at = UtcDateTime::parse_rfc3339(&value.modified_at)?;
327        let secret_id: SecretId = value.identifier.parse()?;
328        let commit_hash = CommitHash(value.commit.as_slice().try_into()?);
329        let meta: AeadPack = decode(&value.meta).await?;
330        let secret: AeadPack = decode(&value.secret).await?;
331        let commit = VaultCommit(commit_hash, VaultEntry(meta, secret));
332
333        Ok(SecretRecord {
334            row_id: value.row_id,
335            created_at,
336            modified_at,
337            secret_id,
338            commit,
339        })
340    }
341}
342
343/// Folder entity.
344pub struct FolderEntity<'conn, C>
345where
346    C: Deref<Target = Connection>,
347{
348    conn: &'conn C,
349}
350
351impl<'conn> FolderEntity<'conn, Box<Connection>> {
352    /// Query to find all secrets in a folder.
353    pub fn find_all_secrets_query() -> sql::Select {
354        secret_select_columns(sql::Select::new())
355            .from("folder_secrets")
356            .where_clause("folder_id=?1")
357    }
358
359    /// Compute the vault for a folder in the database.
360    pub async fn compute_folder_vault(
361        client: &Client,
362        folder_id: &VaultId,
363    ) -> Result<Vault> {
364        let folder_id = *folder_id;
365
366        let (folder_row, secret_rows) = client
367            .conn_and_then(move |conn| {
368                let folder_entity = FolderEntity::new(&conn);
369                let folder_row = folder_entity.find_one(&folder_id)?;
370                let secret_rows =
371                    folder_entity.load_secrets(folder_row.row_id)?;
372                Ok::<_, Error>((folder_row, secret_rows))
373            })
374            .await?;
375
376        let folder_record = FolderRecord::from_row(folder_row).await?;
377        let mut vault = folder_record.into_vault()?;
378        for row in secret_rows {
379            let record = SecretRecord::from_row(row).await?;
380            vault.insert_entry(record.secret_id, record.commit);
381        }
382        Ok(vault)
383    }
384}
385
386impl<'conn> FolderEntity<'conn, Transaction<'conn>> {
387    /// Create a folder and the secrets in a vault.
388    ///
389    /// If a folder with the same identifier already exists
390    /// it is updated and any existing secrets are deleted
391    /// before inserting the new collection of secrets in the vault.
392    pub async fn upsert_folder_and_secrets(
393        client: &Client,
394        account_id: i64,
395        vault: &Vault,
396    ) -> Result<(i64, HashMap<SecretId, i64>)> {
397        let folder_id = *vault.id();
398
399        let meta = if let Some(meta) = vault.header().meta() {
400            Some(encode(meta).await?)
401        } else {
402            None
403        };
404        let salt = vault.salt().cloned();
405        let seed = vault.seed().map(|s| s.as_ref().to_vec());
406
407        let shared_access = if !vault.shared_access().is_empty() {
408            Some(encode(vault.shared_access()).await?)
409        } else {
410            None
411        };
412
413        let folder_row = FolderRow::new_insert_parts(
414            vault.summary(),
415            salt,
416            meta,
417            seed,
418            shared_access,
419        )?;
420
421        let mut secret_rows = Vec::new();
422        for (secret_id, commit) in vault.iter() {
423            let VaultCommit(commit, entry) = commit;
424            secret_rows.push(SecretRow::new(secret_id, commit, entry).await?);
425        }
426
427        client
428            .conn_mut_and_then(move |conn| {
429                let tx = conn.transaction()?;
430                let folder_entity = FolderEntity::new(&tx);
431
432                let folder_id = if let Some(row) =
433                    folder_entity.find_optional(&folder_id)?
434                {
435                    folder_entity.update_folder(&folder_id, &folder_row)?;
436                    folder_entity.delete_all_secrets(row.row_id)?;
437                    row.row_id
438                } else {
439                    folder_entity.insert_folder(account_id, &folder_row)?
440                };
441
442                let secret_ids = folder_entity.insert_folder_secrets(
443                    folder_id,
444                    secret_rows.as_slice(),
445                )?;
446                tx.commit()?;
447                Ok::<_, Error>((folder_id, secret_ids))
448            })
449            .await
450    }
451
452    /// Replace all secrets for a folder using a transaction.
453    pub async fn replace_all_secrets(
454        client: Client,
455        folder_id: &VaultId,
456        vault: &Vault,
457    ) -> Result<()> {
458        let folder_id = *folder_id;
459        let mut insert_secrets = Vec::new();
460        for (secret_id, commit) in vault.iter() {
461            let VaultCommit(commit, entry) = commit;
462            insert_secrets
463                .push(SecretRow::new(secret_id, commit, entry).await?);
464        }
465
466        let folder_update_row = FolderRow::new_update(vault).await?;
467        client
468            .conn_mut(move |conn| {
469                let tx = conn.transaction()?;
470                let folder = FolderEntity::new(&tx);
471                let folder_row = folder.find_one(&folder_id)?;
472                folder.delete_all_secrets(folder_row.row_id)?;
473                for secret_row in insert_secrets {
474                    folder.insert_secret_by_row_id(
475                        folder_row.row_id,
476                        &secret_row,
477                    )?;
478                }
479                folder.update_folder(&folder_id, &folder_update_row)?;
480                tx.commit()?;
481                Ok(())
482            })
483            .await
484            .map_err(Error::from)?;
485        Ok(())
486    }
487}
488
489impl<'conn, C> FolderEntity<'conn, C>
490where
491    C: Deref<Target = Connection>,
492{
493    /// Create a new folder entity.
494    pub fn new(conn: &'conn C) -> Self {
495        Self { conn }
496    }
497
498    fn select_folder<'a>(
499        &'a self,
500        use_identifier: bool,
501    ) -> StdResult<CachedStatement<'a>, SqlError> {
502        let query = folder_select_columns(sql::Select::new()).from("folders");
503
504        let query = if use_identifier {
505            query.where_clause("identifier = ?1")
506        } else {
507            query.where_clause("folder_id = ?1")
508        };
509        self.conn.prepare_cached(&query.as_string())
510    }
511
512    /// Find a folder in the database.
513    pub fn find_one(
514        &self,
515        // FIXME: require account_id?
516        folder_id: &VaultId,
517    ) -> StdResult<FolderRow, SqlError> {
518        let mut stmt = self.select_folder(true)?;
519        stmt.query_row([folder_id.to_string()], |row| row.try_into())
520    }
521
522    /// Find an optional folder in the database.
523    pub fn find_optional(
524        &self,
525        // FIXME: require account_id?
526        folder_id: &VaultId,
527    ) -> StdResult<Option<FolderRow>, SqlError> {
528        let mut stmt = self.select_folder(true)?;
529        stmt.query_row([folder_id.to_string()], |row| {
530            let row: FolderRow = row.try_into()?;
531            Ok(row)
532        })
533        .optional()
534    }
535
536    /// Find a folder in the database by primary key.
537    pub fn find_by_row_id(
538        &self,
539        folder_id: i64,
540    ) -> StdResult<FolderRow, SqlError> {
541        let mut stmt = self.select_folder(false)?;
542        stmt.query_row([folder_id], |row| row.try_into())
543    }
544
545    /// Try to find a login folder for an account.
546    pub fn find_login_folder(&self, account_id: i64) -> Result<FolderRow> {
547        self.find_login_folder_optional(account_id)?
548            .ok_or_else(|| Error::NoLoginFolder(account_id))
549    }
550
551    /// Try to find an optional login folder for an account.
552    pub fn find_login_folder_optional(
553        &self,
554        account_id: i64,
555    ) -> StdResult<Option<FolderRow>, SqlError> {
556        let query = folder_select_columns(sql::Select::new())
557            .from("folders")
558            .left_join(
559                "account_login_folder login ON folders.folder_id = login.folder_id",
560            )
561            .where_clause("folders.account_id=?1")
562            .where_and("login.account_id=?1");
563
564        let mut stmt = self.conn.prepare_cached(&query.as_string())?;
565        stmt.query_row([account_id], |row| row.try_into())
566            .optional()
567    }
568
569    /// Try to find a device folder for an account.
570    pub fn find_device_folder(
571        &self,
572        account_id: i64,
573    ) -> StdResult<Option<FolderRow>, SqlError> {
574        let query = folder_select_columns(sql::Select::new())
575            .from("folders")
576            .left_join(
577                "account_device_folder device ON folders.folder_id = device.folder_id",
578            )
579            .where_clause("folders.account_id=?1")
580            .where_and("device.account_id=?1");
581
582        let mut stmt = self.conn.prepare_cached(&query.as_string())?;
583        stmt.query_row([account_id], |row| row.try_into())
584            .optional()
585    }
586
587    /// List user folders for an account.
588    ///
589    /// Does not include the identity and device folders.
590    pub fn list_user_folders(
591        &self,
592        account_id: i64,
593    ) -> Result<Vec<FolderRow>> {
594        let query = folder_select_columns(sql::Select::new())
595            .from("folders")
596            .left_join(
597                "account_login_folder login ON folders.folder_id = login.folder_id",
598            )
599            .left_join(
600                "account_device_folder device ON folders.folder_id = device.folder_id",
601            )
602            .left_join(
603                "account_shared_folder shared ON folders.folder_id = shared.folder_id",
604            )
605            .where_clause("(folders.account_id=?1 OR shared.account_id=?1)")
606            .where_and("login.folder_id IS NULL")
607            .where_and("device.folder_id IS NULL");
608
609        let mut stmt = self.conn.prepare_cached(&query.as_string())?;
610
611        fn convert_row(row: &Row<'_>) -> Result<FolderRow> {
612            Ok(row.try_into()?)
613        }
614
615        let rows = stmt.query_and_then([account_id], convert_row)?;
616        let mut folders = Vec::new();
617        for row in rows {
618            folders.push(row?);
619        }
620        Ok(folders)
621    }
622
623    /// Update the name of a folder.
624    pub fn update_name(&self, folder_id: &VaultId, name: &str) -> Result<()> {
625        let modified_at = UtcDateTime::default().to_rfc3339()?;
626        let query = sql::Update::new()
627            .update("folders")
628            .set("name = ?1, modified_at = ?2")
629            .where_clause("identifier = ?3");
630        let mut stmt = self.conn.prepare_cached(&query.as_string())?;
631        stmt.execute((name, modified_at, folder_id.to_string()))?;
632        Ok(())
633    }
634
635    /// Update the folder flags.
636    pub fn update_flags(
637        &self,
638        folder_id: &VaultId,
639        flags: &VaultFlags,
640    ) -> Result<()> {
641        let flags = flags.bits().to_le_bytes();
642        let modified_at = UtcDateTime::default().to_rfc3339()?;
643        let query = sql::Update::new()
644            .update("folders")
645            .set("flags = ?1, modified_at = ?2")
646            .where_clause("identifier = ?3");
647        let mut stmt = self.conn.prepare_cached(&query.as_string())?;
648        stmt.execute((flags, modified_at, folder_id.to_string()))?;
649        Ok(())
650    }
651
652    /// Update the folder meta data.
653    pub fn update_meta(
654        &self,
655        folder_id: &VaultId,
656        meta: &[u8],
657    ) -> Result<()> {
658        let modified_at = UtcDateTime::default().to_rfc3339()?;
659        let query = sql::Update::new()
660            .update("folders")
661            .set("meta = ?1, modified_at = ?2")
662            .where_clause("identifier = ?3");
663        let mut stmt = self.conn.prepare_cached(&query.as_string())?;
664        stmt.execute((meta, modified_at, folder_id.to_string()))?;
665        Ok(())
666    }
667
668    /// Create the folder entity in the database.
669    pub fn insert_folder(
670        &self,
671        account_id: i64,
672        folder_row: &FolderRow,
673    ) -> StdResult<i64, SqlError> {
674        let query = sql::Insert::new()
675            .insert_into(
676                r#"
677                folders
678                (
679                    account_id,
680                    created_at,
681                    modified_at,
682                    identifier,
683                    name,
684                    salt,
685                    meta,
686                    seed,
687                    version,
688                    cipher,
689                    kdf,
690                    flags,
691                    shared_access
692                )
693            "#,
694            )
695            .values(
696                "(?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13)",
697            );
698
699        let mut stmt = self.conn.prepare_cached(&query.as_string())?;
700        stmt.execute((
701            &account_id,
702            &folder_row.created_at,
703            &folder_row.modified_at,
704            &folder_row.identifier,
705            &folder_row.name,
706            &folder_row.salt,
707            &folder_row.meta,
708            &folder_row.seed,
709            &folder_row.version,
710            &folder_row.cipher,
711            &folder_row.kdf,
712            &folder_row.flags,
713            &folder_row.shared_access,
714        ))?;
715
716        Ok(self.conn.last_insert_rowid())
717    }
718
719    /// Update the folder entity in the database.
720    pub fn update_folder(
721        &self,
722        folder_id: &VaultId,
723        folder_row: &FolderRow,
724    ) -> StdResult<(), SqlError> {
725        let query = sql::Update::new()
726            .update("folders")
727            .set(
728                r#"
729                    modified_at = ?1,
730                    identifier = ?2,
731                    name = ?3,
732                    salt = ?4,
733                    meta = ?5,
734                    seed = ?6,
735                    version = ?7,
736                    cipher = ?8,
737                    kdf = ?9,
738                    flags = ?10,
739                    shared_access = ?11
740                 "#,
741            )
742            .where_clause("identifier=?12");
743        let mut stmt = self.conn.prepare_cached(&query.as_string())?;
744        stmt.execute((
745            &folder_row.modified_at,
746            &folder_row.identifier,
747            &folder_row.name,
748            &folder_row.salt,
749            &folder_row.meta,
750            &folder_row.seed,
751            &folder_row.version,
752            &folder_row.cipher,
753            &folder_row.kdf,
754            &folder_row.flags,
755            &folder_row.shared_access,
756            folder_id.to_string(),
757        ))?;
758
759        Ok(())
760    }
761
762    /// Create folder secret rows.
763    pub fn insert_folder_secrets(
764        &self,
765        folder_id: i64,
766        rows: &[SecretRow],
767    ) -> Result<HashMap<SecretId, i64>> {
768        let mut secret_ids = HashMap::new();
769        for secret_row in rows {
770            let identifier: SecretId = secret_row.identifier.parse()?;
771            let secret_id =
772                self.insert_secret_by_row_id(folder_id, secret_row)?;
773            secret_ids.insert(identifier, secret_id);
774        }
775        Ok(secret_ids)
776    }
777
778    /// Create folder secret.
779    pub fn insert_secret(
780        &self,
781        folder_id: &VaultId,
782        secret_row: &SecretRow,
783    ) -> StdResult<i64, SqlError> {
784        let row = self.find_one(folder_id)?;
785        self.insert_secret_by_row_id(row.row_id, secret_row)
786    }
787
788    /// Insert a secret using the folder row id.
789    pub fn insert_secret_by_row_id(
790        &self,
791        folder_id: i64,
792        secret_row: &SecretRow,
793    ) -> StdResult<i64, SqlError> {
794        // NOTE: we have to use an upsert here as auto merge
795        // NOTE: can try to create secrets that already exist
796        // NOTE: so we handle the conflict situation
797        let query = sql::Insert::new()
798            .insert_into("folder_secrets (folder_id, identifier, commit_hash, meta, secret, created_at, modified_at)")
799            .values("(?1, ?2, ?3, ?4, ?5, ?6, ?7)")
800            .on_conflict(
801            r#"
802                (identifier)
803                DO UPDATE SET
804                    folder_id=excluded.folder_id,
805                    commit_hash=excluded.commit_hash,
806                    meta=excluded.meta,
807                    secret=excluded.secret,
808                    modified_at=excluded.modified_at
809            "#);
810        let mut stmt = self.conn.prepare_cached(&query.as_string())?;
811        stmt.execute((
812            &folder_id,
813            &secret_row.identifier,
814            &secret_row.commit,
815            &secret_row.meta,
816            &secret_row.secret,
817            &secret_row.created_at,
818            &secret_row.modified_at,
819        ))?;
820        Ok(self.conn.last_insert_rowid())
821    }
822
823    /// Find a folder secret.
824    pub fn find_secret(
825        &self,
826        folder_id: &VaultId,
827        secret_id: &SecretId,
828    ) -> StdResult<Option<SecretRow>, SqlError> {
829        let row = self.find_one(folder_id)?;
830        let query = secret_select_columns(sql::Select::new())
831            .from("folder_secrets")
832            .where_clause("folder_id=?1")
833            .where_and("identifier=?2");
834
835        let mut stmt = self.conn.prepare_cached(&query.as_string())?;
836        stmt.query_row((row.row_id, secret_id.to_string()), |row| {
837            let row: SecretRow = row.try_into()?;
838            Ok(row)
839        })
840        .optional()
841    }
842
843    /// Update a folder secret.
844    pub fn update_secret(
845        &self,
846        folder_id: &VaultId,
847        secret_row: &SecretRow,
848    ) -> Result<bool> {
849        let modified_at = UtcDateTime::default().to_rfc3339()?;
850        let row = self.find_one(folder_id)?;
851        let query = sql::Update::new()
852            .update("folder_secrets")
853            .set(
854                r#"
855
856                    modified_at=?1,
857                    commit_hash=?2,
858                    meta=?3, 
859                    secret=?4
860                 "#,
861            )
862            .where_clause("folder_id=?5")
863            .where_and("identifier = ?6");
864
865        let mut stmt = self.conn.prepare_cached(&query.as_string())?;
866        let affected_rows = stmt.execute((
867            modified_at,
868            &secret_row.commit,
869            &secret_row.meta,
870            &secret_row.secret,
871            row.row_id,
872            &secret_row.identifier,
873        ))?;
874        Ok(affected_rows > 0)
875    }
876
877    /// Load secret rows.
878    pub fn load_secrets(&self, folder_row_id: i64) -> Result<Vec<SecretRow>> {
879        let query = secret_select_columns(sql::Select::new())
880            .from("folder_secrets")
881            .where_clause("folder_id=?1");
882        let mut stmt = self.conn.prepare_cached(&query.as_string())?;
883
884        fn convert_row(row: &Row<'_>) -> Result<SecretRow> {
885            Ok(row.try_into()?)
886        }
887
888        let rows = stmt.query_and_then([folder_row_id], convert_row)?;
889        let mut secrets = Vec::new();
890        for row in rows {
891            secrets.push(row?);
892        }
893        Ok(secrets)
894    }
895
896    /// List secret ids.
897    pub fn list_secret_ids(
898        &self,
899        folder_id: &VaultId,
900    ) -> Result<Vec<SecretId>> {
901        let folder = self.find_one(folder_id)?;
902        let query = sql::Select::new()
903            .select("identifier")
904            .from("folder_secrets")
905            .where_clause("folder_id=?1");
906        let mut stmt = self.conn.prepare_cached(&query.as_string())?;
907
908        fn convert_row(row: &Row<'_>) -> Result<SecretId> {
909            let id: String = row.get(0)?;
910            Ok(id.parse()?)
911        }
912
913        let rows = stmt.query_and_then([folder.row_id], convert_row)?;
914        let mut secrets = Vec::new();
915        for row in rows {
916            secrets.push(row?);
917        }
918        Ok(secrets)
919    }
920
921    /// Delete a folder.
922    pub fn delete_folder(
923        &self,
924        folder_id: &VaultId,
925    ) -> StdResult<bool, SqlError> {
926        let row = self.find_one(folder_id)?;
927        let query = sql::Delete::new()
928            .delete_from("folders")
929            .where_clause("folder_id = ?1");
930        let mut stmt = self.conn.prepare_cached(&query.as_string())?;
931        let affected_rows = stmt.execute([row.row_id])?;
932        Ok(affected_rows > 0)
933    }
934
935    /// Delete folder secret.
936    pub fn delete_secret(
937        &self,
938        folder_id: &VaultId,
939        secret_id: &SecretId,
940    ) -> StdResult<bool, SqlError> {
941        let row = self.find_one(folder_id)?;
942        let query = sql::Delete::new()
943            .delete_from("folder_secrets")
944            .where_clause("folder_id = ?1")
945            .where_and("identifier = ?2");
946        let mut stmt = self.conn.prepare_cached(&query.as_string())?;
947        let affected_rows =
948            stmt.execute((row.row_id, secret_id.to_string()))?;
949        Ok(affected_rows > 0)
950    }
951
952    /// Delete all folder secrets.
953    fn delete_all_secrets(
954        &self,
955        folder_id: i64,
956    ) -> StdResult<usize, SqlError> {
957        let query = sql::Delete::new()
958            .delete_from("folder_secrets")
959            .where_clause("folder_id = ?1");
960        let mut stmt = self.conn.prepare_cached(&query.as_string())?;
961        stmt.execute([folder_id])
962    }
963}