env2bws/
import_payload.rs

1//! Structured representation of Bitwarden Secrets Manager import JSON format
2use crate::{DotEnvFile, EnvVar};
3use uuid::Uuid;
4
5/// Represents a single project as found in the Bitwarden Secrets Manager import JSON format.
6#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
7pub struct Project {
8    pub id: uuid::Uuid,
9    pub name: String,
10}
11
12/// Represents a single secret as found in the Bitwarden Secrets Manager import JSON format.
13#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
14#[serde(rename_all = "camelCase")]
15pub struct Secret {
16    pub key: String,
17    pub value: String,
18    pub note: String,
19    pub project_ids: Vec<uuid::Uuid>,
20    pub id: uuid::Uuid,
21}
22
23impl Secret {
24    /// Parses an individual secret from a given [`EnvVar`] with an optional `project_id`.
25    fn from_env_var(value: EnvVar, project_id: Option<uuid::Uuid>) -> Self {
26        Self {
27            key: value.key,
28            value: value.value,
29            note: value.comment.unwrap_or_default(),
30            project_ids: project_id.map_or(vec![], |id| vec![id]),
31            id: value.temp_id,
32        }
33    }
34}
35
36/// Represents the entirety of the Bitwarden Secrets Manager import JSON format.
37#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
38pub struct ImportPayload {
39    pub projects: Vec<Project>,
40    pub secrets: Vec<Secret>,
41}
42
43/// The way in which all new secrets may (or may not) be assigned to projects in Bitwarden Secrets
44/// Manager.
45pub enum ProjectAssignment {
46    None,
47    Existing(uuid::Uuid),
48    New(String),
49}
50
51impl ImportPayload {
52    /// Constructs a new representation of the import JSON from a parsed [`DotEnvFile`] using the
53    /// provided [`ProjectAssignment`] strategy.
54    pub fn from_dotenv(dotenv: DotEnvFile, project_assignment: ProjectAssignment) -> Self {
55        // Empty vector of projects means no projects are to be created
56        let mut projects: Vec<Project> = vec![];
57
58        // Determine the ID of the project that all secrets will be assigned to (if any)
59        let assigned_id = match project_assignment {
60            // If existing case, assign the provided ID to the project
61            ProjectAssignment::Existing(id) => Some(id),
62            // If new case, create a new project declaration with random UUID and assign the ID to
63            // the project
64            ProjectAssignment::New(name) => {
65                let id = Uuid::new_v4();
66                projects.push(Project { id, name });
67                Some(id)
68            }
69            // If none case, assign no project ID to the secrets
70            ProjectAssignment::None => None,
71        };
72
73        Self {
74            projects,
75            secrets: dotenv
76                .iter()
77                .map(|v| Secret::from_env_var(v.clone(), assigned_id))
78                .collect(),
79        }
80    }
81}
82
83#[cfg(test)]
84mod payload_tests {
85    use fake::{Fake, Faker};
86
87    use super::*;
88
89    #[test]
90    fn leaves_project_blank_on_secrets_when_none_supplied() {
91        let dotenv = Faker.fake::<DotEnvFile>();
92        let payload = ImportPayload::from_dotenv(dotenv, ProjectAssignment::None);
93
94        // No new projects listed
95        assert_eq!(payload.projects.len(), 0);
96
97        // No projects listed on any secret
98        payload
99            .secrets
100            .iter()
101            .for_each(|secret| assert_eq!(secret.project_ids.len(), 0));
102    }
103
104    #[test]
105    fn defines_new_project_and_sets_for_secrets() {
106        let dotenv = Faker.fake::<DotEnvFile>();
107        let payload = ImportPayload::from_dotenv(dotenv, ProjectAssignment::New(Faker.fake()));
108
109        // One new project listed
110        assert_eq!(payload.projects.len(), 1);
111        let project_id = payload
112            .projects
113            .first()
114            .expect("could not get first project")
115            .id;
116
117        // All secrets assigned to new project's id
118        payload.secrets.iter().for_each(|secret| {
119            assert_eq!(secret.project_ids.len(), 1);
120            assert_eq!(
121                secret
122                    .project_ids
123                    .first()
124                    .expect("could not get project id for secret"),
125                &project_id
126            )
127        });
128    }
129
130    #[test]
131    fn sets_existing_project_for_secrets() {
132        let dotenv = Faker.fake::<DotEnvFile>();
133        let project_id = Faker.fake::<Uuid>();
134        let payload =
135            ImportPayload::from_dotenv(dotenv, ProjectAssignment::Existing(project_id.clone()));
136
137        // No new projects listed
138        assert_eq!(payload.projects.len(), 0);
139
140        // All secrets assigned to existing project id
141        payload.secrets.iter().for_each(|secret| {
142            assert_eq!(secret.project_ids.len(), 1);
143            assert_eq!(
144                secret
145                    .project_ids
146                    .first()
147                    .expect("could not get project id for secret"),
148                &project_id
149            )
150        });
151    }
152}