From 7d55563556bbcac244c2458cac50f8cf4b0b53f7 Mon Sep 17 00:00:00 2001 From: Christian Visintin Date: Tue, 4 Jan 2022 18:31:58 +0100 Subject: [PATCH] Aws s3 connection parameters extension (#89) * Aws s3 connection parameters extension * Changed 'save password?' popup to 'change secrets?' * missing docs --- CHANGELOG.md | 5 + Cargo.lock | 6 +- docs/de/man.md | 9 +- docs/es/man.md | 9 +- docs/fr/man.md | 9 +- docs/it/man.md | 11 +- docs/man.md | 9 +- docs/zh-CN/man.md | 8 +- src/config/bookmarks.rs | 22 +- src/config/serialization.rs | 6 + src/filetransfer/builder.rs | 22 +- src/filetransfer/params.rs | 55 ++- src/main.rs | 2 + src/system/bookmarks_client.rs | 130 ++++++- src/system/sshkey_storage.rs | 55 +-- src/ui/activities/auth/bookmarks.rs | 4 + .../activities/auth/components/bookmarks.rs | 2 +- src/ui/activities/auth/components/form.rs | 357 +++++++++++++++++- src/ui/activities/auth/components/mod.rs | 3 +- src/ui/activities/auth/misc.rs | 32 +- src/ui/activities/auth/mod.rs | 12 + src/ui/activities/auth/update.rs | 28 +- src/ui/activities/auth/view.rs | 142 ++++++- src/ui/activities/setup/components/ssh.rs | 4 +- src/ui/activities/setup/components/theme.rs | 2 +- 25 files changed, 830 insertions(+), 114 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 95e42c6..929c3b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -57,6 +57,11 @@ Released on FIXME: - migrated application to tui-realm 1.x - Improved application performance - Changed the buffer size to **65535** (was 65536) for transfer I/O +- **Aws s3 connection parameters extension** 🦊: + - Added `Access Key` to Aws-s3 connection parameters + - Added `Security Access Key` to Aws-s3 connection parameters + - Added `Security token` to Aws-s3 connection parameters + - Added `Session token` to Aws-s3 connection parameters - **SSH Config** - Added `ssh config` parameter in configuration - It is now possible to specify the ssh configuration file to use diff --git a/Cargo.lock b/Cargo.lock index 1aa0900..801117f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2021,7 +2021,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e0bccbcf40c8938196944a3da0e133e031a33f4d6b72db3bda3cc556e361905d" dependencies = [ "lazy_static", - "parking_lot 0.11.2", + "parking_lot 0.10.2", "serial_test_derive", ] @@ -2479,9 +2479,9 @@ dependencies = [ [[package]] name = "tuirealm" -version = "1.3.0" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69e5c7137a0bd92feadea98033a1849fe51c83d23f7761b866e8700a3d6f1de7" +checksum = "8b1919cf08ce9eda3b968870ecab04267d684adc22f2cdc13bb4a3846c89eab4" dependencies = [ "bitflags 1.3.2", "crossterm", diff --git a/docs/de/man.md b/docs/de/man.md index 0dafeb2..9934044 100644 --- a/docs/de/man.md +++ b/docs/de/man.md @@ -112,11 +112,14 @@ Password can be basically provided through 3 ways when address argument is provi ## Aws S3 credentials 🦊 In order to connect to an Aws S3 bucket you must obviously provide some credentials. -There are basically two ways to achieve this, and as you've probably already noticed you **can't** do that via the authentication form. +There are basically three ways to achieve this. So these are the ways you can provide the credentials for s3: -1. Use your credentials file: just configure the AWS cli via `aws configure` and your credentials should already be located at `~/.aws/credentials`. In case you're using a profile different from `default`, just provide it in the profile field in the authentication form. -2. **Environment variables**: you can always provide your credentials as environment variables. Keep in mind that these credentials **will always override** the credentials located in the `credentials` file. See how to configure the environment below: +1. Authentication form: + 1. You can provide the `access_key` (should be mandatory), the `secret_access_key` (should be mandatory), `security_token` and the `session_token` + 2. If you save the s3 connection as a bookmark, these credentials will be saved as an encrypted AES-256/BASE64 string in your bookmarks file (except for the security token and session token which are meant to be temporary credentials). +2. Use your credentials file: just configure the AWS cli via `aws configure` and your credentials should already be located at `~/.aws/credentials`. In case you're using a profile different from `default`, just provide it in the profile field in the authentication form. +3. **Environment variables**: you can always provide your credentials as environment variables. Keep in mind that these credentials **will always override** the credentials located in the `credentials` file. See how to configure the environment below: These should always be mandatory: diff --git a/docs/es/man.md b/docs/es/man.md index 67c97a3..e60155f 100644 --- a/docs/es/man.md +++ b/docs/es/man.md @@ -112,11 +112,14 @@ La contraseña se puede proporcionar básicamente a través de 3 formas cuando s ## Credenciales de AWS S3 🦊 Para conectarse a un bucket de Aws S3, obviamente debe proporcionar algunas credenciales. -Básicamente, hay dos formas de lograr esto, y como probablemente ya hayas notado, **no puedes** hacerlo a través del formulario de autenticación. +Básicamente, hay tres formas de lograr esto. Entonces, estas son las formas en que puede proporcionar las credenciales para s3: -1. Use su archivo de credenciales: simplemente configure la cli de AWS a través de `aws configure` y sus credenciales ya deberían estar ubicadas en`~/.aws/credentials`. En caso de que esté usando un perfil diferente al "predeterminado", simplemente proporciónelo en el campo de perfil en el formulario de autenticación. -2. **Variables de entorno**: siempre puede proporcionar sus credenciales como variables de entorno. Tenga en cuenta que estas credenciales **siempre anularán** las credenciales ubicadas en el archivo `credentials`. Vea cómo configurar el entorno a continuación: +1. Authentication form: + 1. Puede proporcionar la `access_key` (debería ser obligatoria), la `secret_access_kedy` (debería ser obligatoria), el `security_token` y el `session_token` + 2. Si guarda la conexión s3 como marcador, estas credenciales se guardarán como una cadena AES-256 / BASE64 cifrada en su archivo de marcadores (excepto el token de seguridad y el token de sesión, que deben ser credenciales temporales). +2. Use su archivo de credenciales: simplemente configure la cli de AWS a través de `aws configure` y sus credenciales ya deberían estar ubicadas en`~/.aws/credentials`. En caso de que esté usando un perfil diferente al "predeterminado", simplemente proporciónelo en el campo de perfil en el formulario de autenticación. +3. **Variables de entorno**: siempre puede proporcionar sus credenciales como variables de entorno. Tenga en cuenta que estas credenciales **siempre anularán** las credenciales ubicadas en el archivo `credentials`. Vea cómo configurar el entorno a continuación: Estos siempre deben ser obligatorios: diff --git a/docs/fr/man.md b/docs/fr/man.md index b84bd9a..5db0c96 100644 --- a/docs/fr/man.md +++ b/docs/fr/man.md @@ -110,11 +110,14 @@ Le mot de passe peut être fourni de 3 manières lorsque l'argument d'adresse es ## Identifiants AWS S3 🦊 Afin de vous connecter à un compartiment Aws S3, vous devez évidemment fournir des informations d'identification. -Il existe essentiellement deux manières d'y parvenir, et comme vous l'avez probablement déjà remarqué, vous ne pouvez **pas** le faire via le formulaire d'authentification. +Il existe essentiellement trois manières d'y parvenir. Voici donc les moyens de fournir les informations d'identification pour s3 : -1. Utilisez votre fichier d'informations d'identification : configurez simplement l'AWS cli via `aws configure` et vos informations d'identification doivent déjà se trouver dans `~/.aws/credentials`. Si vous utilisez un profil différent de "default", fournissez-le simplement dans le champ profile du formulaire d'authentification. -2. **Variables d'environnement** : vous pouvez toujours fournir vos informations d'identification en tant que variables d'environnement. Gardez à l'esprit que ces informations d'identification **remplaceront toujours** les informations d'identification situées dans le fichier « credentials ». Voir comment configurer l'environnement ci-dessous : +1. Authentication form: + 1. Vous pouvez fournir le `access_key` (devrait être obligatoire), le `secret_access_key` (devrait être obligatoire), `security_token` et le `session_token` + 2. Si vous enregistrez la connexion s3 en tant que signet, ces informations d'identification seront enregistrées en tant que chaîne AES-256/BASE64 cryptée dans votre fichier de signets (à l'exception du jeton de sécurité et du jeton de session qui sont censés être des informations d'identification temporaires). +2. Utilisez votre fichier d'informations d'identification : configurez simplement l'AWS cli via `aws configure` et vos informations d'identification doivent déjà se trouver dans `~/.aws/credentials`. Si vous utilisez un profil différent de "default", fournissez-le simplement dans le champ profile du formulaire d'authentification. +3. **Variables d'environnement** : vous pouvez toujours fournir vos informations d'identification en tant que variables d'environnement. Gardez à l'esprit que ces informations d'identification **remplaceront toujours** les informations d'identification situées dans le fichier « credentials ». Voir comment configurer l'environnement ci-dessous : Ceux-ci devraient toujours être obligatoires: diff --git a/docs/it/man.md b/docs/it/man.md index b90264c..7a0cd42 100644 --- a/docs/it/man.md +++ b/docs/it/man.md @@ -107,11 +107,14 @@ Quando si usa l'argomento indirizzo non è possibile fornire la password diretta ## Credenziali Aws S3 🦊 Per connettersi ad un bucket S3 devi come già saprai fornire le credenziali fornite da AWS. -Ci sono due modi per passare queste credenziali a termscp e come avrai già notato **non puoi** farlo dal form di autenticazione. -Questi sono quindi i due modi per passare le chiavi: +Ci sono tre modi per passare queste credenziali a termscp. +Questi sono quindi i tre modi per passare le chiavi: -1. Utilizza il file delle credenziali s3: configurando aws via `aws configure` le tue credenziali dovrebbero già venir salvate in `~/.aws/credentials`. Nel caso tu debba usare un profile diverso da `default`, puoi fornire un profilo diverso nell'authentication form. -2. **Variabili d'ambiente**: nel caso il primo metodo non sia utilizzabile, puoi comunque fornirle come variabili d'ambiente. Considera però che queste variabili sovrascriveranno sempre le credenziali situate nel file credentials. Vediamo come impostarle: +1. Form di autenticazione: + 1. Puoi fornire la `access_key` (dovrebbe essere obbligatoria), la `secret_access_key` (dovrebbe essere obbligatoria), il `security_token` ed il `session_token` + 2. Se salvi la connessione s3 come segnalibro e decidi di salvare la password, questi parametri verranno salvati nel file dei segnalibri criptati con AES-256/BASE64; ad eccezion fatta per i due token, che dovrebbero essere credenziali temporanee, quindi inutili da salvare. +2. Utilizza il file delle credenziali s3: configurando aws via `aws configure` le tue credenziali dovrebbero già venir salvate in `~/.aws/credentials`. Nel caso tu debba usare un profile diverso da `default`, puoi fornire un profilo diverso nell'authentication form. +3. **Variabili d'ambiente**: nel caso il primo metodo non sia utilizzabile, puoi comunque fornirle come variabili d'ambiente. Considera però che queste variabili sovrascriveranno sempre le credenziali situate nel file credentials. Vediamo come impostarle: Queste sono sempre obbligatorie: diff --git a/docs/man.md b/docs/man.md index 4eef1e0..6f454ff 100644 --- a/docs/man.md +++ b/docs/man.md @@ -110,11 +110,14 @@ Password can be basically provided through 3 ways when address argument is provi ## Aws S3 credentials 🦊 In order to connect to an Aws S3 bucket you must obviously provide some credentials. -There are basically two ways to achieve this, and as you've probably already noticed you **can't** do that via the authentication form. +There are basically three ways to achieve this: So these are the ways you can provide the credentials for s3: -1. Use your credentials file: just configure the AWS cli via `aws configure` and your credentials should already be located at `~/.aws/credentials`. In case you're using a profile different from `default`, just provide it in the profile field in the authentication form. -2. **Environment variables**: you can always provide your credentials as environment variables. Keep in mind that these credentials **will always override** the credentials located in the `credentials` file. See how to configure the environment below: +1. Authentication form: + 1. You can provide the `access_key` (should be mandatory), the `secret_access_key` (should be mandatory), `security_token` and the `session_token` + 2. If you save the s3 connection as a bookmark, these credentials will be saved as an encrypted AES-256/BASE64 string in your bookmarks file (except for the security token and session token which are meant to be temporary credentials). +2. Use your credentials file: just configure the AWS cli via `aws configure` and your credentials should already be located at `~/.aws/credentials`. In case you're using a profile different from `default`, just provide it in the profile field in the authentication form. +3. **Environment variables**: you can always provide your credentials as environment variables. Keep in mind that these credentials **will always override** the credentials located in the `credentials` file. See how to configure the environment below: These should always be mandatory: diff --git a/docs/zh-CN/man.md b/docs/zh-CN/man.md index 470f9b9..1effb41 100644 --- a/docs/zh-CN/man.md +++ b/docs/zh-CN/man.md @@ -108,11 +108,13 @@ s3://buckethead@eu-central-1:default:/assets ## Aws S3 凭证 为了连接到 Aws S3 存储桶,您显然必须提供一些凭据。 -基本上有两种方法可以实现这一点,而且您可能已经注意到您**不能**通过身份验证表单来做到这一点。 因此,您可以通过以下方式为 s3 提供凭据: -1. 使用您的凭证文件:只需通过`aws configure` 配置AWS cli,您的凭证应该已经位于`~/.aws/credentials`。 如果您使用的配置文件不同于“默认”,只需在身份验证表单的配置文件字段中提供它。 -2. **环境变量**: 您始终可以将您的凭据作为环境变量提供。 请记住,这些凭据**将始终覆盖**位于 `credentials` 文件中的凭据。 下面看看如何配置环境: +1. 认证形式: + 1. 您可以提供 `access_key`(应该是强制性的)、`secret_access_key`(应该是强制性的)、`security_token` 和`session_token` + 2. 如果您将 s3 连接保存为书签,这些凭据将在您的书签文件中保存为加密的 AES-256/BASE64 字符串(安全令牌和会话令牌除外,它们是临时凭据)。. +2. 使用您的凭证文件:只需通过`aws configure` 配置AWS cli,您的凭证应该已经位于`~/.aws/credentials`。 如果您使用的配置文件不同于“默认”,只需在身份验证表单的配置文件字段中提供它。 +3. **环境变量**: 您始终可以将您的凭据作为环境变量提供。 请记住,这些凭据**将始终覆盖**位于 `credentials` 文件中的凭据。 下面看看如何配置环境: 这些应该始终是强制性的: diff --git a/src/config/bookmarks.rs b/src/config/bookmarks.rs index 86a01b7..7c76af2 100644 --- a/src/config/bookmarks.rs +++ b/src/config/bookmarks.rs @@ -66,6 +66,8 @@ pub struct S3Params { pub bucket: String, pub region: String, pub profile: Option, + pub access_key: Option, + pub secret_access_key: Option, } // -- impls @@ -122,6 +124,8 @@ impl From for S3Params { bucket: params.bucket_name, region: params.region, profile: params.profile, + access_key: params.access_key, + secret_access_key: params.secret_access_key, } } } @@ -129,6 +133,8 @@ impl From for S3Params { impl From for AwsS3Params { fn from(params: S3Params) -> Self { AwsS3Params::new(params.bucket, params.region, params.profile) + .access_key(params.access_key) + .secret_access_key(params.secret_access_key) } } @@ -227,7 +233,11 @@ mod tests { #[test] fn bookmark_from_s3_ftparams() { - let params = ProtocolParams::AwsS3(AwsS3Params::new("omar", "eu-west-1", Some("test"))); + let params = ProtocolParams::AwsS3( + AwsS3Params::new("omar", "eu-west-1", Some("test")) + .access_key(Some("pippo")) + .secret_access_key(Some("pluto")), + ); let params: FileTransferParams = FileTransferParams::new(FileTransferProtocol::AwsS3, params); let bookmark = Bookmark::from(params); @@ -240,6 +250,8 @@ mod tests { assert_eq!(s3.bucket.as_str(), "omar"); assert_eq!(s3.region.as_str(), "eu-west-1"); assert_eq!(s3.profile.as_deref().unwrap(), "test"); + assert_eq!(s3.access_key.as_deref().unwrap(), "pippo"); + assert_eq!(s3.secret_access_key.as_deref().unwrap(), "pluto"); } #[test] @@ -272,7 +284,9 @@ mod tests { s3: Some(S3Params { bucket: String::from("veeso"), region: String::from("eu-west-1"), - profile: None, + profile: Some(String::from("default")), + access_key: Some(String::from("pippo")), + secret_access_key: Some(String::from("pluto")), }), }; let params = FileTransferParams::from(bookmark); @@ -280,6 +294,8 @@ mod tests { let gparams = params.params.s3_params().unwrap(); assert_eq!(gparams.bucket_name.as_str(), "veeso"); assert_eq!(gparams.region.as_str(), "eu-west-1"); - assert_eq!(gparams.profile, None); + assert_eq!(gparams.profile.as_deref().unwrap(), "default"); + assert_eq!(gparams.access_key.as_deref().unwrap(), "pippo"); + assert_eq!(gparams.secret_access_key.as_deref().unwrap(), "pluto"); } } diff --git a/src/config/serialization.rs b/src/config/serialization.rs index 84198c9..038f4c1 100644 --- a/src/config/serialization.rs +++ b/src/config/serialization.rs @@ -421,6 +421,8 @@ mod tests { assert_eq!(s3.bucket.as_str(), "veeso"); assert_eq!(s3.region.as_str(), "eu-west-1"); assert_eq!(s3.profile.as_deref().unwrap(), "default"); + assert_eq!(s3.access_key.as_deref().unwrap(), "pippo"); + assert_eq!(s3.secret_access_key.as_deref().unwrap(), "pluto"); } #[test] @@ -470,6 +472,8 @@ mod tests { bucket: "veeso".to_string(), region: "eu-west-1".to_string(), profile: None, + access_key: None, + secret_access_key: None, }), }, ); @@ -531,6 +535,8 @@ mod tests { bucket = "veeso" region = "eu-west-1" profile = "default" + access_key = "pippo" + secret_access_key = "pluto" [recents] ISO20201215T094000Z = { address = "172.16.104.10", port = 22, protocol = "SCP", username = "root" } diff --git a/src/filetransfer/builder.rs b/src/filetransfer/builder.rs index cbd8cda..aa906bb 100644 --- a/src/filetransfer/builder.rs +++ b/src/filetransfer/builder.rs @@ -77,6 +77,18 @@ impl Builder { if let Some(profile) = params.profile { client = client.profile(profile); } + if let Some(access_key) = params.access_key { + client = client.access_key(access_key); + } + if let Some(secret_access_key) = params.secret_access_key { + client = client.secret_access_key(secret_access_key); + } + if let Some(security_token) = params.security_token { + client = client.security_token(security_token); + } + if let Some(session_token) = params.session_token { + client = client.session_token(session_token); + } client } @@ -124,7 +136,7 @@ impl Builder { /// Make ssh storage from `ConfigClient` if possible, empty otherwise (empty is implicit if degraded) fn make_ssh_storage(config_client: &ConfigClient) -> SshKeyStorage { - SshKeyStorage::storage_from_config(config_client) + SshKeyStorage::from(config_client) } } @@ -138,7 +150,13 @@ mod test { #[test] fn should_build_aws_s3_fs() { - let params = ProtocolParams::AwsS3(AwsS3Params::new("omar", "eu-west-1", Some("test"))); + let params = ProtocolParams::AwsS3( + AwsS3Params::new("omar", "eu-west-1", Some("test")) + .access_key(Some("pippo")) + .secret_access_key(Some("pluto")) + .security_token(Some("omar")) + .session_token(Some("gerry-scotti")), + ); let config_client = get_config_client(); let _ = Builder::build(FileTransferProtocol::AwsS3, params, &config_client); } diff --git a/src/filetransfer/params.rs b/src/filetransfer/params.rs index f60c435..0168367 100644 --- a/src/filetransfer/params.rs +++ b/src/filetransfer/params.rs @@ -61,6 +61,10 @@ pub struct AwsS3Params { pub bucket_name: String, pub region: String, pub profile: Option, + pub access_key: Option, + pub secret_access_key: Option, + pub security_token: Option, + pub session_token: Option, } impl FileTransferParams { @@ -102,6 +106,7 @@ impl ProtocolParams { } } + /// Get a mutable reference to the inner generic protocol params pub fn mut_generic_params(&mut self) -> Option<&mut GenericProtocolParams> { match self { ProtocolParams::Generic(params) => Some(params), @@ -167,8 +172,36 @@ impl AwsS3Params { bucket_name: bucket.as_ref().to_string(), region: region.as_ref().to_string(), profile: profile.map(|x| x.as_ref().to_string()), + access_key: None, + secret_access_key: None, + security_token: None, + session_token: None, } } + + /// Construct aws s3 params with provided access key + pub fn access_key>(mut self, key: Option) -> Self { + self.access_key = key.map(|x| x.as_ref().to_string()); + self + } + + /// Construct aws s3 params with provided secret_access_key + pub fn secret_access_key>(mut self, key: Option) -> Self { + self.secret_access_key = key.map(|x| x.as_ref().to_string()); + self + } + + /// Construct aws s3 params with provided security_token + pub fn security_token>(mut self, key: Option) -> Self { + self.security_token = key.map(|x| x.as_ref().to_string()); + self + } + + /// Construct aws s3 params with provided session_token + pub fn session_token>(mut self, key: Option) -> Self { + self.session_token = key.map(|x| x.as_ref().to_string()); + self + } } #[cfg(test)] @@ -206,11 +239,31 @@ mod test { } #[test] - fn params_aws_s3() { + fn should_init_aws_s3_params() { let params: AwsS3Params = AwsS3Params::new("omar", "eu-west-1", Some("test")); assert_eq!(params.bucket_name.as_str(), "omar"); assert_eq!(params.region.as_str(), "eu-west-1"); assert_eq!(params.profile.as_deref().unwrap(), "test"); + assert!(params.access_key.is_none()); + assert!(params.secret_access_key.is_none()); + assert!(params.security_token.is_none()); + assert!(params.session_token.is_none()); + } + + #[test] + fn should_init_aws_s3_params_with_optionals() { + let params: AwsS3Params = AwsS3Params::new("omar", "eu-west-1", Some("test")) + .access_key(Some("pippo")) + .secret_access_key(Some("pluto")) + .security_token(Some("omar")) + .session_token(Some("gerry-scotti")); + assert_eq!(params.bucket_name.as_str(), "omar"); + assert_eq!(params.region.as_str(), "eu-west-1"); + assert_eq!(params.profile.as_deref().unwrap(), "test"); + assert_eq!(params.access_key.as_deref().unwrap(), "pippo"); + assert_eq!(params.secret_access_key.as_deref().unwrap(), "pluto"); + assert_eq!(params.security_token.as_deref().unwrap(), "omar"); + assert_eq!(params.session_token.as_deref().unwrap(), "gerry-scotti"); } #[test] diff --git a/src/main.rs b/src/main.rs index 1579474..ba62d0b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -227,7 +227,9 @@ fn parse_args(args: Args) -> Result { fn read_password(run_opts: &mut RunOpts) -> Result<(), String> { // Initialize client if necessary if let Some(remote) = run_opts.remote.as_mut() { + // Ask password for generic params if let Some(mut params) = remote.params.mut_generic_params() { + // Ask password only if generic protocol params if params.password.is_none() { // Ask password if unspecified params.password = match rpassword::read_password_from_tty(Some("Password: ")) { diff --git a/src/system/bookmarks_client.rs b/src/system/bookmarks_client.rs index 9b9f0d9..132985c 100644 --- a/src/system/bookmarks_client.rs +++ b/src/system/bookmarks_client.rs @@ -167,7 +167,38 @@ impl BookmarksClient { *pwd = decrypted_pwd; } Err(err) => { - error!("Failed to decrypt password for bookmark: {}", err); + error!("Failed to decrypt `password` for bookmark {}: {}", key, err); + } + } + } + // Decrypt AWS-S3 params + if let Some(s3) = entry.s3.as_mut() { + // Access key + if let Some(access_key) = s3.access_key.as_mut() { + match self.decrypt_str(access_key.as_str()) { + Ok(plain) => { + *access_key = plain; + } + Err(err) => { + error!( + "Failed to decrypt `access_key` for bookmark {}: {}", + key, err + ); + } + } + } + // Secret access key + if let Some(secret_access_key) = s3.secret_access_key.as_mut() { + match self.decrypt_str(secret_access_key.as_str()) { + Ok(plain) => { + *secret_access_key = plain; + } + Err(err) => { + error!( + "Failed to decrypt `secret_access_key` for bookmark {}: {}", + key, err + ); + } } } } @@ -190,9 +221,13 @@ impl BookmarksClient { // Make bookmark info!("Added bookmark {}", name); let mut host: Bookmark = self.make_bookmark(params); - // If not save_password, set password to `None` + // If not save_password, set secrets to `None` if !save_password { host.password = None; + if let Some(s3) = host.s3.as_mut() { + s3.access_key = None; + s3.secret_access_key = None; + } } self.hosts.bookmarks.insert(name, host); } @@ -221,6 +256,10 @@ impl BookmarksClient { let mut host: Bookmark = self.make_bookmark(params); // Null password for recents host.password = None; + if let Some(s3) = host.s3.as_mut() { + s3.access_key = None; + s3.secret_access_key = None; + } // Check if duplicated for (key, value) in &self.hosts.recents { if *value == host { @@ -321,6 +360,15 @@ impl BookmarksClient { if let Some(pwd) = bookmark.password { bookmark.password = Some(self.encrypt_str(pwd.as_str())); } + // Encrypt aws s3 params + if let Some(s3) = bookmark.s3.as_mut() { + if let Some(access_key) = s3.access_key.as_mut() { + *access_key = self.encrypt_str(access_key.as_str()); + } + if let Some(secret_access_key) = s3.secret_access_key.as_mut() { + *secret_access_key = self.encrypt_str(secret_access_key.as_str()); + } + } bookmark } @@ -346,7 +394,7 @@ impl BookmarksClient { mod tests { use super::*; - use crate::filetransfer::params::GenericProtocolParams; + use crate::filetransfer::params::{AwsS3Params, GenericProtocolParams}; use crate::filetransfer::{FileTransferProtocol, ProtocolParams}; use pretty_assertions::assert_eq; @@ -441,6 +489,69 @@ mod tests { assert_eq!(bookmark.4, None); } + #[test] + fn should_make_s3_bookmark_with_secrets() { + let tmp_dir: tempfile::TempDir = TempDir::new().ok().unwrap(); + let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path()); + // Initialize a new bookmarks client + let mut client: BookmarksClient = + BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16).unwrap(); + // Add s3 bookmark + client.add_bookmark("my-bucket", make_s3_ftparams(), true); + // Verify bookmark + let bookmark = client.get_bookmark("my-bucket").unwrap(); + assert_eq!(bookmark.protocol, FileTransferProtocol::AwsS3); + let params = bookmark.params.s3_params().unwrap(); + assert_eq!(params.access_key.as_deref().unwrap(), "pippo"); + assert_eq!(params.profile.as_deref().unwrap(), "test"); + assert_eq!(params.secret_access_key.as_deref().unwrap(), "pluto"); + assert_eq!(params.bucket_name.as_str(), "omar"); + assert_eq!(params.region.as_str(), "eu-west-1"); + } + + #[test] + fn should_make_s3_bookmark_without_secrets() { + let tmp_dir: tempfile::TempDir = TempDir::new().ok().unwrap(); + let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path()); + // Initialize a new bookmarks client + let mut client: BookmarksClient = + BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16).unwrap(); + // Add s3 bookmark + client.add_bookmark("my-bucket", make_s3_ftparams(), false); + // Verify bookmark + let bookmark = client.get_bookmark("my-bucket").unwrap(); + assert_eq!(bookmark.protocol, FileTransferProtocol::AwsS3); + let params = bookmark.params.s3_params().unwrap(); + assert_eq!(params.profile.as_deref().unwrap(), "test"); + assert_eq!(params.bucket_name.as_str(), "omar"); + assert_eq!(params.region.as_str(), "eu-west-1"); + // secrets + assert_eq!(params.access_key, None); + assert_eq!(params.secret_access_key, None); + } + + #[test] + fn should_make_s3_recent() { + let tmp_dir: tempfile::TempDir = TempDir::new().ok().unwrap(); + let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path()); + // Initialize a new bookmarks client + let mut client: BookmarksClient = + BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16).unwrap(); + // Add s3 bookmark + client.add_recent(make_s3_ftparams()); + // Verify bookmark + let bookmark = client.iter_recents().next().unwrap(); + let bookmark = client.get_recent(bookmark).unwrap(); + assert_eq!(bookmark.protocol, FileTransferProtocol::AwsS3); + let params = bookmark.params.s3_params().unwrap(); + assert_eq!(params.profile.as_deref().unwrap(), "test"); + assert_eq!(params.bucket_name.as_str(), "omar"); + assert_eq!(params.region.as_str(), "eu-west-1"); + // secrets + assert_eq!(params.access_key, None); + assert_eq!(params.secret_access_key, None); + } + #[test] fn test_system_bookmarks_manipulate_bookmarks() { @@ -734,6 +845,19 @@ mod tests { FileTransferParams::new(protocol, params) } + fn make_s3_ftparams() -> FileTransferParams { + FileTransferParams::new( + FileTransferProtocol::AwsS3, + ProtocolParams::AwsS3( + AwsS3Params::new("omar", "eu-west-1", Some("test")) + .access_key(Some("pippo")) + .secret_access_key(Some("pluto")) + .security_token(Some("omar")) + .session_token(Some("gerry-scotti")), + ), + ) + } + fn ftparams_to_tup( params: FileTransferParams, ) -> (String, u16, FileTransferProtocol, String, Option) { diff --git a/src/system/sshkey_storage.rs b/src/system/sshkey_storage.rs index d25b743..0924bee 100644 --- a/src/system/sshkey_storage.rs +++ b/src/system/sshkey_storage.rs @@ -37,32 +37,6 @@ pub struct SshKeyStorage { } impl SshKeyStorage { - /// Create a `SshKeyStorage` starting from a `ConfigClient` - pub fn storage_from_config(cfg_client: &ConfigClient) -> Self { - let mut hosts: HashMap = - HashMap::with_capacity(cfg_client.iter_ssh_keys().count()); - debug!("Setting up SSH key storage"); - // Iterate over keys - for key in cfg_client.iter_ssh_keys() { - match cfg_client.get_ssh_key(key) { - Ok(host) => match host { - Some((addr, username, rsa_key_path)) => { - let key_name: String = Self::make_mapkey(&addr, &username); - hosts.insert(key_name, rsa_key_path); - } - None => continue, - }, - Err(err) => { - error!("Failed to get SSH key for {}: {}", key, err); - continue; - } - } - info!("Got SSH key for {}", key); - } - // Return storage - SshKeyStorage { hosts } - } - /// Create an empty ssh key storage; used in case `ConfigClient` is not available #[cfg(test)] pub fn empty() -> Self { @@ -92,6 +66,33 @@ impl SshKeyStorageT for SshKeyStorage { } } +impl From<&ConfigClient> for SshKeyStorage { + fn from(cfg_client: &ConfigClient) -> Self { + let mut hosts: HashMap = + HashMap::with_capacity(cfg_client.iter_ssh_keys().count()); + debug!("Setting up SSH key storage"); + // Iterate over keys + for key in cfg_client.iter_ssh_keys() { + match cfg_client.get_ssh_key(key) { + Ok(host) => match host { + Some((addr, username, rsa_key_path)) => { + let key_name: String = Self::make_mapkey(&addr, &username); + hosts.insert(key_name, rsa_key_path); + } + None => continue, + }, + Err(err) => { + error!("Failed to get SSH key for {}: {}", key, err); + continue; + } + } + info!("Got SSH key for {}", key); + } + // Return storage + SshKeyStorage { hosts } + } +} + #[cfg(test)] mod tests { @@ -113,7 +114,7 @@ mod tests { .add_ssh_key("192.168.1.31", "pi", "piroporopero") .is_ok()); // Create ssh key storage - let storage: SshKeyStorage = SshKeyStorage::storage_from_config(&client); + let storage: SshKeyStorage = SshKeyStorage::from(&client); // Verify key exists let mut exp_key_path: PathBuf = key_path.clone(); exp_key_path.push("pi@192.168.1.31.key"); diff --git a/src/ui/activities/auth/bookmarks.rs b/src/ui/activities/auth/bookmarks.rs index d3ac9bb..4f89b8f 100644 --- a/src/ui/activities/auth/bookmarks.rs +++ b/src/ui/activities/auth/bookmarks.rs @@ -229,5 +229,9 @@ impl AuthActivity { self.mount_s3_bucket(params.bucket_name.as_str()); self.mount_s3_region(params.region.as_str()); self.mount_s3_profile(params.profile.as_deref().unwrap_or("")); + self.mount_s3_access_key(params.access_key.as_deref().unwrap_or("")); + self.mount_s3_secret_access_key(params.secret_access_key.as_deref().unwrap_or("")); + self.mount_s3_security_token(params.security_token.as_deref().unwrap_or("")); + self.mount_s3_session_token(params.session_token.as_deref().unwrap_or("")); } } diff --git a/src/ui/activities/auth/components/bookmarks.rs b/src/ui/activities/auth/components/bookmarks.rs index 8ad9661..75c162f 100644 --- a/src/ui/activities/auth/components/bookmarks.rs +++ b/src/ui/activities/auth/components/bookmarks.rs @@ -346,7 +346,7 @@ impl BookmarkSavePassword { .value(0) .rewind(true) .foreground(color) - .title("Save password?", Alignment::Center), + .title("Save secrets?", Alignment::Center), } } } diff --git a/src/ui/activities/auth/components/form.rs b/src/ui/activities/auth/components/form.rs index d45150e..2847b90 100644 --- a/src/ui/activities/auth/components/form.rs +++ b/src/ui/activities/auth/components/form.rs @@ -177,7 +177,7 @@ impl Component for InputAddress { } Event::Keyboard(KeyEvent { code: Key::Char(ch), - modifiers: KeyModifiers::NONE, + modifiers: KeyModifiers::NONE | KeyModifiers::SHIFT, }) => { self.perform(Cmd::Type(ch)); Some(Msg::None) @@ -263,7 +263,7 @@ impl Component for InputPort { } Event::Keyboard(KeyEvent { code: Key::Char(ch), - modifiers: KeyModifiers::NONE, + modifiers: KeyModifiers::NONE | KeyModifiers::SHIFT, }) => { self.perform(Cmd::Type(ch)); Some(Msg::None) @@ -348,7 +348,7 @@ impl Component for InputUsername { } Event::Keyboard(KeyEvent { code: Key::Char(ch), - modifiers: KeyModifiers::NONE, + modifiers: KeyModifiers::NONE | KeyModifiers::SHIFT, }) => { self.perform(Cmd::Type(ch)); Some(Msg::None) @@ -432,7 +432,7 @@ impl Component for InputPassword { } Event::Keyboard(KeyEvent { code: Key::Char(ch), - modifiers: KeyModifiers::NONE, + modifiers: KeyModifiers::NONE | KeyModifiers::SHIFT, }) => { self.perform(Cmd::Type(ch)); Some(Msg::None) @@ -517,7 +517,7 @@ impl Component for InputS3Bucket { } Event::Keyboard(KeyEvent { code: Key::Char(ch), - modifiers: KeyModifiers::NONE, + modifiers: KeyModifiers::NONE | KeyModifiers::SHIFT, }) => { self.perform(Cmd::Type(ch)); Some(Msg::None) @@ -537,7 +537,7 @@ impl Component for InputS3Bucket { } } -// -- s3 bucket +// -- s3 region #[derive(MockComponent)] pub struct InputS3Region { @@ -602,7 +602,7 @@ impl Component for InputS3Region { } Event::Keyboard(KeyEvent { code: Key::Char(ch), - modifiers: KeyModifiers::NONE, + modifiers: KeyModifiers::NONE | KeyModifiers::SHIFT, }) => { self.perform(Cmd::Type(ch)); Some(Msg::None) @@ -622,7 +622,7 @@ impl Component for InputS3Region { } } -// -- s3 bucket +// -- s3 profile #[derive(MockComponent)] pub struct InputS3Profile { @@ -687,7 +687,7 @@ impl Component for InputS3Profile { } Event::Keyboard(KeyEvent { code: Key::Char(ch), - modifiers: KeyModifiers::NONE, + modifiers: KeyModifiers::NONE | KeyModifiers::SHIFT, }) => { self.perform(Cmd::Type(ch)); Some(Msg::None) @@ -708,3 +708,342 @@ impl Component for InputS3Profile { } } } + +// -- s3 access key + +#[derive(MockComponent)] +pub struct InputS3AccessKey { + component: Input, +} + +impl InputS3AccessKey { + pub fn new(access_key: &str, color: Color) -> Self { + Self { + component: Input::default() + .borders( + Borders::default() + .color(color) + .modifiers(BorderType::Rounded), + ) + .foreground(color) + .placeholder("AKIA...", Style::default().fg(Color::Rgb(128, 128, 128))) + .title("Access key", Alignment::Left) + .input_type(InputType::Text) + .value(access_key), + } + } +} + +impl Component for InputS3AccessKey { + fn on(&mut self, ev: Event) -> Option { + match ev { + Event::Keyboard(KeyEvent { + code: Key::Left, .. + }) => { + self.perform(Cmd::Move(Direction::Left)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Right, .. + }) => { + self.perform(Cmd::Move(Direction::Right)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Home, .. + }) => { + self.perform(Cmd::GoTo(Position::Begin)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { code: Key::End, .. }) => { + self.perform(Cmd::GoTo(Position::End)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Delete, .. + }) => { + self.perform(Cmd::Cancel); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Backspace, + .. + }) => { + self.perform(Cmd::Delete); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Char(ch), + modifiers: KeyModifiers::NONE | KeyModifiers::SHIFT, + }) => { + self.perform(Cmd::Type(ch)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Enter, .. + }) => Some(Msg::Form(FormMsg::Connect)), + Event::Keyboard(KeyEvent { + code: Key::Down, .. + }) => Some(Msg::Ui(UiMsg::S3AccessKeyBlurDown)), + Event::Keyboard(KeyEvent { code: Key::Up, .. }) => { + Some(Msg::Ui(UiMsg::S3AccessKeyBlurUp)) + } + Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => { + Some(Msg::Ui(UiMsg::ParamsFormBlur)) + } + _ => None, + } + } +} + +#[derive(MockComponent)] +pub struct InputS3SecretAccessKey { + component: Input, +} + +impl InputS3SecretAccessKey { + pub fn new(secret_access_key: &str, color: Color) -> Self { + Self { + component: Input::default() + .borders( + Borders::default() + .color(color) + .modifiers(BorderType::Rounded), + ) + .foreground(color) + .title("Secret access key", Alignment::Left) + .input_type(InputType::Password('*')) + .value(secret_access_key), + } + } +} + +impl Component for InputS3SecretAccessKey { + fn on(&mut self, ev: Event) -> Option { + match ev { + Event::Keyboard(KeyEvent { + code: Key::Left, .. + }) => { + self.perform(Cmd::Move(Direction::Left)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Right, .. + }) => { + self.perform(Cmd::Move(Direction::Right)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Home, .. + }) => { + self.perform(Cmd::GoTo(Position::Begin)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { code: Key::End, .. }) => { + self.perform(Cmd::GoTo(Position::End)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Delete, .. + }) => { + self.perform(Cmd::Cancel); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Backspace, + .. + }) => { + self.perform(Cmd::Delete); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Char(ch), + modifiers: KeyModifiers::NONE | KeyModifiers::SHIFT, + }) => { + self.perform(Cmd::Type(ch)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Enter, .. + }) => Some(Msg::Form(FormMsg::Connect)), + Event::Keyboard(KeyEvent { + code: Key::Down, .. + }) => Some(Msg::Ui(UiMsg::S3SecretAccessKeyBlurDown)), + Event::Keyboard(KeyEvent { code: Key::Up, .. }) => { + Some(Msg::Ui(UiMsg::S3SecretAccessKeyBlurUp)) + } + Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => { + Some(Msg::Ui(UiMsg::ParamsFormBlur)) + } + _ => None, + } + } +} + +#[derive(MockComponent)] +pub struct InputS3SecurityToken { + component: Input, +} + +impl InputS3SecurityToken { + pub fn new(security_token: &str, color: Color) -> Self { + Self { + component: Input::default() + .borders( + Borders::default() + .color(color) + .modifiers(BorderType::Rounded), + ) + .foreground(color) + .title("Security token", Alignment::Left) + .input_type(InputType::Password('*')) + .value(security_token), + } + } +} + +impl Component for InputS3SecurityToken { + fn on(&mut self, ev: Event) -> Option { + match ev { + Event::Keyboard(KeyEvent { + code: Key::Left, .. + }) => { + self.perform(Cmd::Move(Direction::Left)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Right, .. + }) => { + self.perform(Cmd::Move(Direction::Right)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Home, .. + }) => { + self.perform(Cmd::GoTo(Position::Begin)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { code: Key::End, .. }) => { + self.perform(Cmd::GoTo(Position::End)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Delete, .. + }) => { + self.perform(Cmd::Cancel); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Backspace, + .. + }) => { + self.perform(Cmd::Delete); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Char(ch), + modifiers: KeyModifiers::NONE | KeyModifiers::SHIFT, + }) => { + self.perform(Cmd::Type(ch)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Enter, .. + }) => Some(Msg::Form(FormMsg::Connect)), + Event::Keyboard(KeyEvent { + code: Key::Down, .. + }) => Some(Msg::Ui(UiMsg::S3SecurityTokenBlurDown)), + Event::Keyboard(KeyEvent { code: Key::Up, .. }) => { + Some(Msg::Ui(UiMsg::S3SecurityTokenBlurUp)) + } + Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => { + Some(Msg::Ui(UiMsg::ParamsFormBlur)) + } + _ => None, + } + } +} + +#[derive(MockComponent)] +pub struct InputS3SessionToken { + component: Input, +} + +impl InputS3SessionToken { + pub fn new(session_token: &str, color: Color) -> Self { + Self { + component: Input::default() + .borders( + Borders::default() + .color(color) + .modifiers(BorderType::Rounded), + ) + .foreground(color) + .title("Session token", Alignment::Left) + .input_type(InputType::Password('*')) + .value(session_token), + } + } +} + +impl Component for InputS3SessionToken { + fn on(&mut self, ev: Event) -> Option { + match ev { + Event::Keyboard(KeyEvent { + code: Key::Left, .. + }) => { + self.perform(Cmd::Move(Direction::Left)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Right, .. + }) => { + self.perform(Cmd::Move(Direction::Right)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Home, .. + }) => { + self.perform(Cmd::GoTo(Position::Begin)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { code: Key::End, .. }) => { + self.perform(Cmd::GoTo(Position::End)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Delete, .. + }) => { + self.perform(Cmd::Cancel); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Backspace, + .. + }) => { + self.perform(Cmd::Delete); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Char(ch), + modifiers: KeyModifiers::NONE | KeyModifiers::SHIFT, + }) => { + self.perform(Cmd::Type(ch)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Enter, .. + }) => Some(Msg::Form(FormMsg::Connect)), + Event::Keyboard(KeyEvent { + code: Key::Down, .. + }) => Some(Msg::Ui(UiMsg::S3SessionTokenBlurDown)), + Event::Keyboard(KeyEvent { code: Key::Up, .. }) => { + Some(Msg::Ui(UiMsg::S3SessionTokenBlurUp)) + } + Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => { + Some(Msg::Ui(UiMsg::ParamsFormBlur)) + } + _ => None, + } + } +} diff --git a/src/ui/activities/auth/components/mod.rs b/src/ui/activities/auth/components/mod.rs index bb014ae..d42bf60 100644 --- a/src/ui/activities/auth/components/mod.rs +++ b/src/ui/activities/auth/components/mod.rs @@ -37,7 +37,8 @@ pub use bookmarks::{ RecentsList, }; pub use form::{ - InputAddress, InputPassword, InputPort, InputS3Bucket, InputS3Profile, InputS3Region, + InputAddress, InputPassword, InputPort, InputS3AccessKey, InputS3Bucket, InputS3Profile, + InputS3Region, InputS3SecretAccessKey, InputS3SecurityToken, InputS3SessionToken, InputUsername, ProtocolRadio, }; pub use popup::{ diff --git a/src/ui/activities/auth/misc.rs b/src/ui/activities/auth/misc.rs index 29b2164..a00d2dc 100644 --- a/src/ui/activities/auth/misc.rs +++ b/src/ui/activities/auth/misc.rs @@ -26,7 +26,7 @@ * SOFTWARE. */ use super::{AuthActivity, FileTransferParams, FileTransferProtocol}; -use crate::filetransfer::params::{AwsS3Params, GenericProtocolParams, ProtocolParams}; +use crate::filetransfer::params::ProtocolParams; use crate::system::auto_update::{Release, Update, UpdateStatus}; use crate::system::notifications::Notification; @@ -68,46 +68,32 @@ impl AuthActivity { &self, protocol: FileTransferProtocol, ) -> Result { - let (address, port, username, password): (String, u16, String, String) = - self.get_generic_params_input(); - if address.is_empty() { + let params = self.get_generic_params_input(); + if params.address.is_empty() { return Err("Invalid host"); } - if port == 0 { + if params.port == 0 { return Err("Invalid port"); } Ok(FileTransferParams { protocol, - params: ProtocolParams::Generic( - GenericProtocolParams::default() - .address(address) - .port(port) - .username(match username.is_empty() { - true => None, - false => Some(username), - }) - .password(match password.is_empty() { - true => None, - false => Some(password), - }), - ), + params: ProtocolParams::Generic(params), entry_directory: None, }) } /// Get input values from fields or return an error if fields are invalid to work as aws s3 pub(super) fn collect_s3_host_params(&self) -> Result { - let (bucket, region, profile): (String, String, Option) = - self.get_s3_params_input(); - if bucket.is_empty() { + let params = self.get_s3_params_input(); + if params.bucket_name.is_empty() { return Err("Invalid bucket"); } - if region.is_empty() { + if params.region.is_empty() { return Err("Invalid region"); } Ok(FileTransferParams { protocol: FileTransferProtocol::AwsS3, - params: ProtocolParams::AwsS3(AwsS3Params::new(bucket, region, profile)), + params: ProtocolParams::AwsS3(params), entry_directory: None, }) } diff --git a/src/ui/activities/auth/mod.rs b/src/ui/activities/auth/mod.rs index 08ee9c3..7d2ea8a 100644 --- a/src/ui/activities/auth/mod.rs +++ b/src/ui/activities/auth/mod.rs @@ -66,9 +66,13 @@ pub enum Id { Protocol, QuitPopup, RecentsList, + S3AccessKey, S3Bucket, S3Profile, S3Region, + S3SecretAccessKey, + S3SecurityToken, + S3SessionToken, Subtitle, Title, Username, @@ -119,12 +123,20 @@ pub enum UiMsg { ProtocolBlurDown, ProtocolBlurUp, RececentsListBlur, + S3AccessKeyBlurDown, + S3AccessKeyBlurUp, S3BucketBlurDown, S3BucketBlurUp, S3ProfileBlurDown, S3ProfileBlurUp, S3RegionBlurDown, S3RegionBlurUp, + S3SecretAccessKeyBlurDown, + S3SecretAccessKeyBlurUp, + S3SecurityTokenBlurDown, + S3SecurityTokenBlurUp, + S3SessionTokenBlurDown, + S3SessionTokenBlurUp, BookmarkNameBlur, SaveBookmarkPasswordBlur, ShowDeleteBookmarkPopup, diff --git a/src/ui/activities/auth/update.rs b/src/ui/activities/auth/update.rs index bbe62d8..76124c2 100644 --- a/src/ui/activities/auth/update.rs +++ b/src/ui/activities/auth/update.rs @@ -203,7 +203,7 @@ impl AuthActivity { .app .active(match self.input_mask() { InputMask::Generic => &Id::Password, - InputMask::AwsS3 => &Id::S3Profile, + InputMask::AwsS3 => &Id::S3SessionToken, }) .is_ok()); } @@ -223,11 +223,35 @@ impl AuthActivity { assert!(self.app.active(&Id::S3Bucket).is_ok()); } UiMsg::S3ProfileBlurDown => { - assert!(self.app.active(&Id::Protocol).is_ok()); + assert!(self.app.active(&Id::S3AccessKey).is_ok()); } UiMsg::S3ProfileBlurUp => { assert!(self.app.active(&Id::S3Region).is_ok()); } + UiMsg::S3AccessKeyBlurDown => { + assert!(self.app.active(&Id::S3SecretAccessKey).is_ok()); + } + UiMsg::S3AccessKeyBlurUp => { + assert!(self.app.active(&Id::S3Profile).is_ok()); + } + UiMsg::S3SecretAccessKeyBlurDown => { + assert!(self.app.active(&Id::S3SecurityToken).is_ok()); + } + UiMsg::S3SecretAccessKeyBlurUp => { + assert!(self.app.active(&Id::S3AccessKey).is_ok()); + } + UiMsg::S3SecurityTokenBlurDown => { + assert!(self.app.active(&Id::S3SessionToken).is_ok()); + } + UiMsg::S3SecurityTokenBlurUp => { + assert!(self.app.active(&Id::S3SecretAccessKey).is_ok()); + } + UiMsg::S3SessionTokenBlurDown => { + assert!(self.app.active(&Id::Protocol).is_ok()); + } + UiMsg::S3SessionTokenBlurUp => { + assert!(self.app.active(&Id::S3SecurityToken).is_ok()); + } UiMsg::SaveBookmarkPasswordBlur => { assert!(self.app.active(&Id::BookmarkName).is_ok()); } diff --git a/src/ui/activities/auth/view.rs b/src/ui/activities/auth/view.rs index 29ce858..322dcd0 100644 --- a/src/ui/activities/auth/view.rs +++ b/src/ui/activities/auth/view.rs @@ -27,7 +27,7 @@ */ // Locals use super::{components, AuthActivity, Context, FileTransferProtocol, Id, InputMask}; -use crate::filetransfer::params::ProtocolParams; +use crate::filetransfer::params::{AwsS3Params, GenericProtocolParams, ProtocolParams}; use crate::filetransfer::FileTransferParams; use crate::utils::ui::draw_area_in; @@ -74,6 +74,10 @@ impl AuthActivity { self.mount_s3_bucket(""); self.mount_s3_profile(""); self.mount_s3_region(""); + self.mount_s3_access_key(""); + self.mount_s3_secret_access_key(""); + self.mount_s3_security_token(""); + self.mount_s3_session_token(""); // Version notice if let Some(version) = self .context() @@ -158,6 +162,7 @@ impl AuthActivity { Constraint::Length(3), // bucket Constraint::Length(3), // region Constraint::Length(3), // profile + Constraint::Length(3), // access_key ] .as_ref(), ) @@ -190,9 +195,11 @@ impl AuthActivity { // Render input mask match self.input_mask() { InputMask::AwsS3 => { - self.app.view(&Id::S3Bucket, f, input_mask[0]); - self.app.view(&Id::S3Region, f, input_mask[1]); - self.app.view(&Id::S3Profile, f, input_mask[2]); + let s3_view_ids = self.get_s3_view(); + self.app.view(&s3_view_ids[0], f, input_mask[0]); + self.app.view(&s3_view_ids[1], f, input_mask[1]); + self.app.view(&s3_view_ids[2], f, input_mask[2]); + self.app.view(&s3_view_ids[3], f, input_mask[3]); } InputMask::Generic => { self.app.view(&Id::Address, f, input_mask[0]); @@ -653,23 +660,83 @@ impl AuthActivity { .is_ok()); } + pub(crate) fn mount_s3_access_key(&mut self, key: &str) { + let password_color = self.theme().auth_password; + assert!(self + .app + .remount( + Id::S3AccessKey, + Box::new(components::InputS3AccessKey::new(key, password_color)), + vec![] + ) + .is_ok()); + } + + pub(crate) fn mount_s3_secret_access_key(&mut self, key: &str) { + let addr_color = self.theme().auth_address; + assert!(self + .app + .remount( + Id::S3SecretAccessKey, + Box::new(components::InputS3SecretAccessKey::new(key, addr_color)), + vec![] + ) + .is_ok()); + } + + pub(crate) fn mount_s3_security_token(&mut self, token: &str) { + let port_color = self.theme().auth_port; + assert!(self + .app + .remount( + Id::S3SecurityToken, + Box::new(components::InputS3SecurityToken::new(token, port_color)), + vec![] + ) + .is_ok()); + } + + pub(crate) fn mount_s3_session_token(&mut self, token: &str) { + let username_color = self.theme().auth_username; + assert!(self + .app + .remount( + Id::S3SessionToken, + Box::new(components::InputS3SessionToken::new(token, username_color)), + vec![] + ) + .is_ok()); + } + // -- query /// Collect input values from view - pub(super) fn get_generic_params_input(&self) -> (String, u16, String, String) { + pub(super) fn get_generic_params_input(&self) -> GenericProtocolParams { let addr: String = self.get_input_addr(); let port: u16 = self.get_input_port(); - let username: String = self.get_input_username(); - let password: String = self.get_input_password(); - (addr, port, username, password) + let username = self.get_input_username(); + let password = self.get_input_password(); + GenericProtocolParams::default() + .address(addr) + .port(port) + .username(username) + .password(password) } /// Collect s3 input values from view - pub(super) fn get_s3_params_input(&self) -> (String, String, Option) { + pub(super) fn get_s3_params_input(&self) -> AwsS3Params { let bucket: String = self.get_input_s3_bucket(); let region: String = self.get_input_s3_region(); let profile: Option = self.get_input_s3_profile(); - (bucket, region, profile) + let access_key = self.get_input_s3_access_key(); + let secret_access_key = self.get_input_s3_secret_access_key(); + let security_token = self.get_input_s3_security_token(); + let session_token = self.get_input_s3_session_token(); + AwsS3Params::new(bucket, region, profile) + .access_key(access_key) + .secret_access_key(secret_access_key) + .security_token(security_token) + .session_token(session_token) } pub(super) fn get_input_addr(&self) -> String { @@ -689,17 +756,17 @@ impl AuthActivity { } } - pub(super) fn get_input_username(&self) -> String { + pub(super) fn get_input_username(&self) -> Option { match self.app.state(&Id::Username) { - Ok(State::One(StateValue::String(x))) => x, - _ => String::new(), + Ok(State::One(StateValue::String(x))) if !x.is_empty() => Some(x), + _ => None, } } - pub(super) fn get_input_password(&self) -> String { + pub(super) fn get_input_password(&self) -> Option { match self.app.state(&Id::Password) { - Ok(State::One(StateValue::String(x))) => x, - _ => String::new(), + Ok(State::One(StateValue::String(x))) if !x.is_empty() => Some(x), + _ => None, } } @@ -724,6 +791,34 @@ impl AuthActivity { } } + pub(super) fn get_input_s3_access_key(&self) -> Option { + match self.app.state(&Id::S3AccessKey) { + Ok(State::One(StateValue::String(x))) if !x.is_empty() => Some(x), + _ => None, + } + } + + pub(super) fn get_input_s3_secret_access_key(&self) -> Option { + match self.app.state(&Id::S3SecretAccessKey) { + Ok(State::One(StateValue::String(x))) if !x.is_empty() => Some(x), + _ => None, + } + } + + pub(super) fn get_input_s3_security_token(&self) -> Option { + match self.app.state(&Id::S3SecurityToken) { + Ok(State::One(StateValue::String(x))) if !x.is_empty() => Some(x), + _ => None, + } + } + + pub(super) fn get_input_s3_session_token(&self) -> Option { + match self.app.state(&Id::S3SessionToken) { + Ok(State::One(StateValue::String(x))) if !x.is_empty() => Some(x), + _ => None, + } + } + /// Get new bookmark params pub(super) fn get_new_bookmark(&self) -> (String, bool) { let name = match self.app.state(&Id::BookmarkName) { @@ -745,7 +840,7 @@ impl AuthActivity { /// Returns the input mask size based on current input mask pub(super) fn input_mask_size(&self) -> u16 { match self.input_mask() { - InputMask::AwsS3 => 9, + InputMask::AwsS3 => 12, InputMask::Generic => 12, } } @@ -785,6 +880,19 @@ impl AuthActivity { } } + /// Get the visible element in the aws-s3 form, based on current focus + fn get_s3_view(&self) -> [Id; 4] { + match self.app.focus() { + Some(&Id::S3SecretAccessKey | &Id::S3SecurityToken | &Id::S3SessionToken) => [ + Id::S3AccessKey, + Id::S3SecretAccessKey, + Id::S3SecurityToken, + Id::S3SessionToken, + ], + _ => [Id::S3Bucket, Id::S3Region, Id::S3Profile, Id::S3AccessKey], + } + } + fn init_global_listener(&mut self) { use tuirealm::event::{Key, KeyEvent, KeyModifiers}; assert!(self diff --git a/src/ui/activities/setup/components/ssh.rs b/src/ui/activities/setup/components/ssh.rs index cbd43d5..c53adae 100644 --- a/src/ui/activities/setup/components/ssh.rs +++ b/src/ui/activities/setup/components/ssh.rs @@ -240,7 +240,7 @@ impl Component for SshHost { } Event::Keyboard(KeyEvent { code: Key::Char(ch), - modifiers: KeyModifiers::NONE, + modifiers: KeyModifiers::NONE | KeyModifiers::SHIFT, }) => { self.perform(Cmd::Type(ch)); Some(Msg::None) @@ -319,7 +319,7 @@ impl Component for SshUsername { } Event::Keyboard(KeyEvent { code: Key::Char(ch), - modifiers: KeyModifiers::NONE, + modifiers: KeyModifiers::NONE | KeyModifiers::SHIFT, }) => { self.perform(Cmd::Type(ch)); Some(Msg::None) diff --git a/src/ui/activities/setup/components/theme.rs b/src/ui/activities/setup/components/theme.rs index 3b93724..872534e 100644 --- a/src/ui/activities/setup/components/theme.rs +++ b/src/ui/activities/setup/components/theme.rs @@ -895,7 +895,7 @@ impl Component for InputColor { } Event::Keyboard(KeyEvent { code: Key::Char(ch), - modifiers: KeyModifiers::NONE, + modifiers: KeyModifiers::NONE | KeyModifiers::SHIFT, }) => { let result = self.perform(Cmd::Type(ch)); self.update_color(result)