1#![allow(
9 dead_code,
10 reason = "While unused now, these states will be consumed as commands move to the CLI."
11)]
12
13use bitwarden_core::GlobalClient;
14use bitwarden_pm::PasswordManagerClient;
15use color_eyre::eyre::{Result, eyre};
16
17use crate::render::CommandResult;
18
19pub trait BwCommand {
45 type Client: ClientState;
46 async fn run(self, client: Self::Client) -> CommandResult;
47}
48
49pub trait BwCommandExt: BwCommand {
61 async fn dispatch(self, ctx: ClientContext) -> CommandResult;
62}
63
64impl<C: BwCommand> BwCommandExt for C {
65 async fn dispatch(self, ctx: ClientContext) -> CommandResult {
66 self.run(C::Client::try_from(ctx)?).await
67 }
68}
69
70mod sealed {
71 pub trait Sealed {}
72}
73
74pub trait ClientState:
79 sealed::Sealed + TryFrom<ClientContext, Error = color_eyre::eyre::Error>
80{
81}
82
83pub struct ClientContext {
85 pub global: GlobalClient,
86 pub user: Option<PasswordManagerClient>,
87}
88
89pub struct LoggedOut {
91 pub global: GlobalClient,
92}
93
94impl sealed::Sealed for LoggedOut {}
95impl ClientState for LoggedOut {}
96
97impl TryFrom<ClientContext> for LoggedOut {
98 type Error = color_eyre::eyre::Error;
99
100 fn try_from(ctx: ClientContext) -> Result<Self> {
101 if ctx.user.is_some() {
102 return Err(eyre!(
103 "You are already logged in. Log out first with `bw logout`."
104 ));
105 }
106 Ok(LoggedOut { global: ctx.global })
107 }
108}
109
110pub struct LoggedIn {
112 pub global: GlobalClient,
113 pub user: PasswordManagerClient,
114}
115
116impl sealed::Sealed for LoggedIn {}
117impl ClientState for LoggedIn {}
118
119impl TryFrom<ClientContext> for LoggedIn {
120 type Error = color_eyre::eyre::Error;
121
122 fn try_from(ctx: ClientContext) -> Result<Self> {
123 let user = ctx
124 .user
125 .ok_or_else(|| eyre!("You are not logged in. Run `bw login` first."))?;
126 Ok(LoggedIn {
127 global: ctx.global,
128 user,
129 })
130 }
131}
132
133pub struct Locked {
135 pub global: GlobalClient,
136 pub user: PasswordManagerClient,
137}
138
139impl sealed::Sealed for Locked {}
140impl ClientState for Locked {}
141
142impl TryFrom<ClientContext> for Locked {
143 type Error = color_eyre::eyre::Error;
144
145 fn try_from(ctx: ClientContext) -> Result<Self> {
146 let user = ctx
147 .user
148 .ok_or_else(|| eyre!("You are not logged in. Run `bw login` first."))?;
149 if user.is_unlocked() {
150 return Err(eyre!(
151 "Your vault is already unlocked. Lock it with `bw lock` first."
152 ));
153 }
154 Ok(Locked {
155 global: ctx.global,
156 user,
157 })
158 }
159}
160
161pub struct Unlocked {
163 pub global: GlobalClient,
164 pub user: PasswordManagerClient,
165}
166
167impl sealed::Sealed for Unlocked {}
168impl ClientState for Unlocked {}
169
170impl TryFrom<ClientContext> for Unlocked {
171 type Error = color_eyre::eyre::Error;
172
173 fn try_from(ctx: ClientContext) -> Result<Self> {
174 let user = ctx
175 .user
176 .ok_or_else(|| eyre!("You are not logged in. Run `bw login` first."))?;
177 if !user.is_unlocked() {
178 return Err(eyre!(
179 "Your vault is locked. Unlock it by setting the session key with `--session` or `BW_SESSION`."
180 ));
181 }
182 Ok(Unlocked {
183 global: ctx.global,
184 user,
185 })
186 }
187}
188
189pub struct AnyState {
191 pub global: GlobalClient,
192 pub user: Option<PasswordManagerClient>,
193}
194
195impl sealed::Sealed for AnyState {}
196impl ClientState for AnyState {}
197
198impl TryFrom<ClientContext> for AnyState {
199 type Error = color_eyre::eyre::Error;
200
201 fn try_from(ctx: ClientContext) -> Result<Self> {
202 Ok(AnyState {
203 global: ctx.global,
204 user: ctx.user,
205 })
206 }
207}
208
209#[cfg(test)]
210mod tests {
211 use std::sync::Once;
212
213 use bitwarden_core::{DeviceType, HostPlatformInfo, init_host_platform_info};
214
215 use super::*;
216
217 static INIT: Once = Once::new();
220
221 fn ensure_platform_info() {
222 INIT.call_once(|| {
223 init_host_platform_info(HostPlatformInfo {
224 user_agent: "bw-tests".to_string(),
225 device_type: DeviceType::SDK,
226 device_identifier: None,
227 bitwarden_client_version: None,
228 bitwarden_package_type: None,
229 });
230 });
231 }
232
233 fn ctx(user: Option<PasswordManagerClient>) -> ClientContext {
234 ensure_platform_info();
235 ClientContext {
236 global: GlobalClient::new(),
237 user,
238 }
239 }
240
241 #[test]
242 fn logged_out_accepts_when_no_user() {
243 assert!(LoggedOut::try_from(ctx(None)).is_ok());
244 }
245
246 #[test]
247 fn logged_out_rejects_when_user_present() {
248 let user = PasswordManagerClient::new(None);
249 assert!(LoggedOut::try_from(ctx(Some(user))).is_err());
250 }
251
252 #[test]
253 fn logged_in_accepts_when_user_present() {
254 let user = PasswordManagerClient::new(None);
255 assert!(LoggedIn::try_from(ctx(Some(user))).is_ok());
256 }
257
258 #[test]
259 fn logged_in_rejects_when_no_user() {
260 assert!(LoggedIn::try_from(ctx(None)).is_err());
261 }
262
263 #[test]
264 fn locked_accepts_when_user_locked() {
265 let user = PasswordManagerClient::new(None);
267 assert!(Locked::try_from(ctx(Some(user))).is_ok());
268 }
269
270 #[test]
271 fn locked_rejects_when_no_user() {
272 assert!(Locked::try_from(ctx(None)).is_err());
273 }
274
275 #[test]
276 fn unlocked_rejects_when_user_locked() {
277 let user = PasswordManagerClient::new(None);
278 assert!(Unlocked::try_from(ctx(Some(user))).is_err());
279 }
280
281 #[test]
282 fn unlocked_rejects_when_no_user() {
283 assert!(Unlocked::try_from(ctx(None)).is_err());
284 }
285
286 #[test]
287 fn any_state_infallible() {
288 assert!(AnyState::try_from(ctx(None)).is_ok());
289 let user = PasswordManagerClient::new(None);
290 assert!(AnyState::try_from(ctx(Some(user))).is_ok());
291 }
292}