From 55e3c1bfdac60e2781364bc1d3a2974a1ac6a43e Mon Sep 17 00:00:00 2001 From: TimeEngineer Date: Fri, 5 Jan 2024 17:03:50 +0100 Subject: [PATCH] CLI: create-workspace --- cli/src/create_workspace.rs | 42 ++++++++++++++++++ cli/src/main.rs | 6 +++ cli/src/tests.rs | 26 +++++++++++ cli/src/utils.rs | 88 +++++++++++++++++++++++++++++++++++-- libparsec/src/lib.rs | 2 +- 5 files changed, 160 insertions(+), 4 deletions(-) create mode 100644 cli/src/create_workspace.rs diff --git a/cli/src/create_workspace.rs b/cli/src/create_workspace.rs new file mode 100644 index 00000000000..67912e175d2 --- /dev/null +++ b/cli/src/create_workspace.rs @@ -0,0 +1,42 @@ +// Parsec Cloud (https://parsec.cloud) Copyright (c) BUSL-1.1 2016-present Scille SAS + +use clap::Args; +use std::path::PathBuf; + +use libparsec::{get_default_config_dir, EntryName}; + +use crate::utils::*; + +#[derive(Args)] +pub struct CreateWorkspace { + /// Parsec config directory + #[arg(short, long, default_value_os_t = get_default_config_dir())] + config_dir: PathBuf, + /// Device slughash + #[arg(short, long)] + device: Option, + /// New workspace name + #[arg(short, long)] + name: EntryName, +} + +pub async fn create_workspace(create_workspace: CreateWorkspace) -> anyhow::Result<()> { + let CreateWorkspace { + config_dir, + device, + name, + } = create_workspace; + + load_client_and_run(config_dir, device, |client| async move { + let handle = start_spinner("Creating workspace"); + + client.user_ops.create_workspace(name).await?; + + handle.done(); + + println!("Workspace has been created"); + + Ok(()) + }) + .await +} diff --git a/cli/src/main.rs b/cli/src/main.rs index 7fd25431a0f..20903b7f239 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -4,6 +4,7 @@ mod bootstrap_organization; mod cancel_invitation; mod claim_invitation; mod create_organization; +mod create_workspace; mod export_recovery_device; mod greet_invitation; mod import_recovery_device; @@ -41,6 +42,8 @@ enum Command { ClaimInvitation(claim_invitation::ClaimInvitation), /// Create new organization CreateOrganization(create_organization::CreateOrganization), + /// Create new workspace + CreateWorkspace(create_workspace::CreateWorkspace), /// Export recovery device ExportRecoveryDevice(export_recovery_device::ExportRecoveryDevice), /// Import recovery device @@ -88,6 +91,9 @@ async fn main() -> anyhow::Result<()> { Command::CreateOrganization(create_organization) => { create_organization::create_organization(create_organization).await } + Command::CreateWorkspace(create_workspace) => { + create_workspace::create_workspace(create_workspace).await + } Command::ExportRecoveryDevice(export_recovery_device) => { export_recovery_device::export_recovery_device(export_recovery_device).await } diff --git a/cli/src/tests.rs b/cli/src/tests.rs index 2549e2df3f6..d776bf42087 100644 --- a/cli/src/tests.rs +++ b/cli/src/tests.rs @@ -532,6 +532,32 @@ async fn list_invitations(tmp_path: TmpPath) { ))); } +#[rstest::rstest] +#[tokio::test] +async fn create_workspace(tmp_path: TmpPath) { + let tmp_path_str = tmp_path.to_str().unwrap(); + let config = get_testenv_config(); + let (url, [alice, ..], _) = run_local_organization(&tmp_path, None, config) + .await + .unwrap(); + + set_env(&tmp_path_str, &url); + + Command::cargo_bin("parsec_cli") + .unwrap() + .args([ + "create-workspace", + "--device", + &alice.slughash(), + "--name", + "new-workspace", + ]) + .assert() + .stdout(predicates::str::contains( + "Creating workspace\nWorkspace has been created", + )); +} + #[rstest::rstest] #[tokio::test] async fn invite_device_dance(tmp_path: TmpPath) { diff --git a/cli/src/utils.rs b/cli/src/utils.rs index 2e1342e0e85..46ec4b3f542 100644 --- a/cli/src/utils.rs +++ b/cli/src/utils.rs @@ -3,9 +3,10 @@ use std::{future::Future, path::PathBuf, sync::Arc}; use libparsec::{ - list_available_devices, load_device, AuthenticatedCmds, AvailableDevice, DeviceAccessStrategy, - DeviceFileType, DeviceLabel, HumanHandle, LocalDevice, Password, ProxyConfig, SASCode, - UserProfile, + internal::{Client, EventBus}, + list_available_devices, load_device, AuthenticatedCmds, AvailableDevice, ClientConfig, + DeviceAccessStrategy, DeviceFileType, DeviceLabel, HumanHandle, LocalDevice, Password, + ProxyConfig, SASCode, UserProfile, }; use terminal_spinners::{SpinnerBuilder, SpinnerHandle, DOTS}; @@ -108,6 +109,87 @@ where Ok(()) } +pub async fn load_client_and_run( + config_dir: PathBuf, + device_slughash: Option, + function: F, +) -> anyhow::Result<()> +where + F: FnOnce(Client) -> Fut, + Fut: Future>, +{ + let devices = list_available_devices(&config_dir).await; + + if let Some(device_slughash) = device_slughash { + let mut possible_devices = vec![]; + + for device in &devices { + if device.slughash().starts_with(&device_slughash) { + possible_devices.push(device); + } + } + + match possible_devices.len() { + 0 => { + println!("Device `{device_slughash}` not found, available devices:"); + format_devices(&devices); + } + 1 => { + let device = possible_devices[0]; + + let device = match device.ty { + DeviceFileType::Password => { + #[cfg(feature = "testenv")] + let password = "test".to_string().into(); + #[cfg(not(feature = "testenv"))] + let password = rpassword::prompt_password("password:")?.into(); + + let access = DeviceAccessStrategy::Password { + key_file: device.key_file_path.clone(), + password, + }; + + // This will fail if the password is invalid, but also if the binary is compiled with fast crypto (see libparsec_crypto) + load_device(&config_dir, &access).await? + } + DeviceFileType::Smartcard => { + let access = DeviceAccessStrategy::Smartcard { + key_file: device.key_file_path.clone(), + }; + + load_device(&config_dir, &access).await? + } + DeviceFileType::Recovery => { + return Err(anyhow::anyhow!( + "Unsupported device file authentication `{:?}`", + device.ty + )); + } + }; + + let client = Client::start( + Arc::new(ClientConfig::default().into()), + EventBus::default(), + device, + ) + .await?; + + function(client).await?; + } + _ => { + println!("Multiple devices found for `{device_slughash}`:"); + format_devices(&devices); + } + } + } else { + println!("Error: Missing option '--device'\n"); + println!("Available devices:"); + format_devices(&devices); + } + + Ok(()) +} + pub fn start_spinner(text: &'static str) -> SpinnerHandle { SpinnerBuilder::new().spinner(&DOTS).text(text).start() } diff --git a/libparsec/src/lib.rs b/libparsec/src/lib.rs index 02cfff39e91..4ebcb5663a7 100644 --- a/libparsec/src/lib.rs +++ b/libparsec/src/lib.rs @@ -30,7 +30,7 @@ pub use workspace::*; pub mod internal { pub use libparsec_client::{ - claimer_retrieve_info, DeviceClaimFinalizeCtx, DeviceClaimInProgress1Ctx, + claimer_retrieve_info, Client, DeviceClaimFinalizeCtx, DeviceClaimInProgress1Ctx, DeviceClaimInProgress2Ctx, DeviceClaimInProgress3Ctx, DeviceClaimInitialCtx, DeviceGreetInProgress1Ctx, DeviceGreetInProgress2Ctx, DeviceGreetInProgress3Ctx, DeviceGreetInProgress4Ctx, DeviceGreetInitialCtx, EventBus, UserClaimFinalizeCtx,