diff --git a/src-tauri/src/ipc.rs b/src-tauri/src/ipc.rs index 663129e..77c3ebc 100644 --- a/src-tauri/src/ipc.rs +++ b/src-tauri/src/ipc.rs @@ -14,6 +14,14 @@ use crate::state::AppState; use crate::terminal; +#[derive(Clone, Debug, Serialize, Deserialize)] +pub enum RequestAction { + Access, + Delete, + Save, +} + + #[derive(Clone, Debug, Serialize, Deserialize)] pub struct AwsRequestNotification { pub client: Client, @@ -31,6 +39,7 @@ pub struct SshRequestNotification { #[derive(Clone, Debug, Serialize, Deserialize)] pub struct DockerRequestNotification { + pub action: RequestAction, pub client: Client, pub server_url: String, } @@ -53,8 +62,8 @@ impl RequestNotificationDetail { Self::Ssh(SshRequestNotification {client, key_name}) } - pub fn new_docker(client: Client, server_url: String) -> Self { - Self::Docker(DockerRequestNotification {client, server_url}) + pub fn new_docker(action: RequestAction, client: Client, server_url: String) -> Self { + Self::Docker(DockerRequestNotification {action, client, server_url}) } } diff --git a/src-tauri/src/srv/creddy_server.rs b/src-tauri/src/srv/creddy_server.rs index b1981e3..ec220c5 100644 --- a/src-tauri/src/srv/creddy_server.rs +++ b/src-tauri/src/srv/creddy_server.rs @@ -9,7 +9,11 @@ use crate::credentials::{ DockerCredential, }; use crate::errors::*; -use crate::ipc::{Approval, RequestNotificationDetail}; +use crate::ipc::{ + Approval, + RequestAction, + RequestNotificationDetail +}; use crate::shortcuts::{self, ShortcutAction}; use crate::state::AppState; use super::{ @@ -58,10 +62,10 @@ async fn handle( server_url, client, app_handle, waiter ).await, CliRequest::StoreDockerCredential(docker_credential) => store_docker_credential( - docker_credential, app_handle, waiter + docker_credential, app_handle, client, waiter ).await, CliRequest::EraseDockerCredential { server_url } => erase_docker_credential( - server_url, app_handle, waiter + server_url, app_handle, client, waiter ).await, CliRequest::InvokeShortcut{ action } => invoke_shortcut(action).await, }; @@ -115,8 +119,8 @@ async fn get_docker_credential( waiter: CloseWaiter<'_>, ) -> Result { let state = app_handle.state::(); - let credential_id = state.credential_id(&server_url).await.unwrap_or(None); - if credential_id.is_none() { + let meta = state.docker_credential_meta(&server_url).await.unwrap_or(None); + if meta.is_none() { return Err( HandlerError::NoCredentials( GetCredentialsError::Load( @@ -126,7 +130,11 @@ async fn get_docker_credential( ); } - let detail = RequestNotificationDetail::new_docker(client, server_url.clone()); + let detail = RequestNotificationDetail::new_docker( + RequestAction::Access, + client, + server_url.clone() + ); let response = super::send_credentials_request(detail, app_handle.clone(), waiter).await?; match response.approval { Approval::Approved => { @@ -142,22 +150,45 @@ async fn get_docker_credential( async fn store_docker_credential( docker_credential: DockerCredential, app_handle: AppHandle, - _waiter: CloseWaiter<'_>, + client: Client, + waiter: CloseWaiter<'_>, ) -> Result { let state = app_handle.state::(); - // eventually ask the frontend to confirm here + // We want to do this before asking for confirmation from the user, because Docker has an annoying + // habit of calling `get` and then immediately turning around and calling `store` with the same + // data. In that case we want to avoid asking for confirmation at all. + match state.get_docker_credential(&docker_credential.server_url).await { + // if there is already a credential with this server_url, and it is unchanged, we're done + Ok(c) if c == docker_credential => return Ok(CliResponse::Empty), + // otherwise we are making an update, so proceed + Ok(_) => (), + // if the app is locked, then this isn't the situation described above, so proceed + Err(GetCredentialsError::Locked) => (), + // if the app is unlocked, and there is no matching credential, proceed + Err(GetCredentialsError::Load(LoadCredentialsError::NoCredentials)) => (), + // any other error is a failure + Err(e) => return Err(e.into()), + }; - // for some reason Docker likes to call `store` immediately with whatever it gets - // back from every `get` operation, so we have to check for an existing credential - let id = state.credential_id(&docker_credential.server_url) + let detail = RequestNotificationDetail::new_docker( + RequestAction::Save, + client, + docker_credential.server_url.clone(), + ); + let response = super::send_credentials_request(detail, app_handle.clone(), waiter).await?; + if matches!(response.approval, Approval::Denied) { + return Err(HandlerError::Denied); + } + + let (id, name) = state.docker_credential_meta(&docker_credential.server_url) .await .map_err(|e| GetCredentialsError::Load(e))? - .unwrap_or_else(|| credentials::random_uuid()); + .unwrap_or_else(|| (credentials::random_uuid(), docker_credential.server_url.clone())); let record = CredentialRecord { id, - name: docker_credential.server_url.clone(), + name, is_default: false, credential: Credential::Docker(docker_credential) }; @@ -169,12 +200,24 @@ async fn store_docker_credential( async fn erase_docker_credential( server_url: String, app_handle: AppHandle, - _waiter: CloseWaiter<'_> + client: Client, + waiter: CloseWaiter<'_> ) -> Result { let state = app_handle.state::(); - // eventually ask the frontend to confirm here - - state.delete_credential_by_name(&server_url).await?; - Ok(CliResponse::Empty) + let detail = RequestNotificationDetail::new_docker( + RequestAction::Delete, + client, + server_url.clone(), + ); + let resp = super::send_credentials_request(detail, app_handle.clone(), waiter).await?; + match resp.approval { + Approval::Approved => { + state.delete_credential_by_name(&server_url).await?; + Ok(CliResponse::Empty) + } + Approval::Denied => { + Err(HandlerError::Denied) + } + } } diff --git a/src-tauri/src/state.rs b/src-tauri/src/state.rs index 9249f03..d7cdcae 100644 --- a/src-tauri/src/state.rs +++ b/src-tauri/src/state.rs @@ -148,13 +148,6 @@ impl AppState { } } - pub async fn credential_id(&self, name: &str) -> Result, LoadCredentialsError> { - let res = sqlx::query_scalar!(r#"SELECT id as "id: Uuid" FROM credentials WHERE name = ?"#, name) - .fetch_optional(&self.pool) - .await; - Ok(res?) - } - pub async fn save_credential(&self, record: CredentialRecord) -> Result<(), SaveCredentialsError> { let session = self.app_session.read().await; let crypto = session.try_get_crypto()?; @@ -337,6 +330,23 @@ impl AppState { Ok(k) } + pub async fn docker_credential_meta( + &self, server_url: &str + ) -> Result, LoadCredentialsError> { + let res = sqlx::query!( + r#"SELECT + c.id as "id: Uuid", + c.name + FROM + credentials c + JOIN docker_credentials d + ON d.id = c.id + WHERE d.server_url = ?"#, + server_url + ).fetch_optional(&self.pool).await?; + Ok(res.map(|row| (row.id, row.name))) + } + pub async fn get_docker_credential(&self, server_url: &str) -> Result { let app_session = self.app_session.read().await; let crypto = app_session.try_get_crypto()?; diff --git a/src/views/approve/CollectResponse.svelte b/src/views/approve/CollectResponse.svelte index d1d2166..8221856 100644 --- a/src/views/approve/CollectResponse.svelte +++ b/src/views/approve/CollectResponse.svelte @@ -26,6 +26,12 @@ }; dispatch('response'); } + + const actionDescriptions = { + Access: 'access your', + Delete: 'delete your', + Save: 'create new', + }; @@ -52,7 +58,7 @@ {:else if $appState.currentRequest.type === 'Ssh'} {appName ? `"${appName}"` : 'An application'} would like to use your SSH key "{$appState.currentRequest.key_name}". {:else if $appState.currentRequest.type === 'Docker'} - {appName ? `"${appName}"` : 'An application'} would like to use your Docker credentials for {$appState.currentRequest.server_url}. + {appName ? `"${appName}"` : 'An application'} would like to {actionDescriptions[$appState.currentRequest.action]} Docker credentials for {$appState.currentRequest.server_url}. {/if} diff --git a/src/views/credentials/DockerCredential.svelte b/src/views/credentials/DockerCredential.svelte index 0c3dfc9..c434a45 100644 --- a/src/views/credentials/DockerCredential.svelte +++ b/src/views/credentials/DockerCredential.svelte @@ -83,6 +83,13 @@ bind:value={local.credential.ServerURL} > + Username + + Password