finish basic Docker credential helper implementation
This commit is contained in:
		| @@ -14,6 +14,14 @@ use crate::state::AppState; | |||||||
| use crate::terminal; | use crate::terminal; | ||||||
|  |  | ||||||
|  |  | ||||||
|  | #[derive(Clone, Debug, Serialize, Deserialize)] | ||||||
|  | pub enum RequestAction { | ||||||
|  |     Access, | ||||||
|  |     Delete, | ||||||
|  |     Save, | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
| #[derive(Clone, Debug, Serialize, Deserialize)] | #[derive(Clone, Debug, Serialize, Deserialize)] | ||||||
| pub struct AwsRequestNotification { | pub struct AwsRequestNotification { | ||||||
|     pub client: Client, |     pub client: Client, | ||||||
| @@ -31,6 +39,7 @@ pub struct SshRequestNotification { | |||||||
|  |  | ||||||
| #[derive(Clone, Debug, Serialize, Deserialize)] | #[derive(Clone, Debug, Serialize, Deserialize)] | ||||||
| pub struct DockerRequestNotification { | pub struct DockerRequestNotification { | ||||||
|  |     pub action: RequestAction, | ||||||
|     pub client: Client, |     pub client: Client, | ||||||
|     pub server_url: String, |     pub server_url: String, | ||||||
| } | } | ||||||
| @@ -53,8 +62,8 @@ impl RequestNotificationDetail { | |||||||
|         Self::Ssh(SshRequestNotification {client, key_name}) |         Self::Ssh(SshRequestNotification {client, key_name}) | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     pub fn new_docker(client: Client, server_url: String) -> Self { |     pub fn new_docker(action: RequestAction, client: Client, server_url: String) -> Self { | ||||||
|         Self::Docker(DockerRequestNotification {client, server_url}) |         Self::Docker(DockerRequestNotification {action, client, server_url}) | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -9,7 +9,11 @@ use crate::credentials::{ | |||||||
|     DockerCredential, |     DockerCredential, | ||||||
| }; | }; | ||||||
| use crate::errors::*; | use crate::errors::*; | ||||||
| use crate::ipc::{Approval, RequestNotificationDetail}; | use crate::ipc::{ | ||||||
|  |     Approval, | ||||||
|  |     RequestAction, | ||||||
|  |     RequestNotificationDetail | ||||||
|  | }; | ||||||
| use crate::shortcuts::{self, ShortcutAction}; | use crate::shortcuts::{self, ShortcutAction}; | ||||||
| use crate::state::AppState; | use crate::state::AppState; | ||||||
| use super::{ | use super::{ | ||||||
| @@ -58,10 +62,10 @@ async fn handle( | |||||||
|             server_url, client, app_handle, waiter |             server_url, client, app_handle, waiter | ||||||
|         ).await, |         ).await, | ||||||
|         CliRequest::StoreDockerCredential(docker_credential) => store_docker_credential( |         CliRequest::StoreDockerCredential(docker_credential) => store_docker_credential( | ||||||
|              docker_credential, app_handle, waiter |              docker_credential, app_handle, client, waiter | ||||||
|         ).await, |         ).await, | ||||||
|         CliRequest::EraseDockerCredential { server_url } => erase_docker_credential( |         CliRequest::EraseDockerCredential { server_url } => erase_docker_credential( | ||||||
|             server_url, app_handle, waiter |             server_url, app_handle, client, waiter | ||||||
|         ).await, |         ).await, | ||||||
|         CliRequest::InvokeShortcut{ action } => invoke_shortcut(action).await, |         CliRequest::InvokeShortcut{ action } => invoke_shortcut(action).await, | ||||||
|     }; |     }; | ||||||
| @@ -115,8 +119,8 @@ async fn get_docker_credential( | |||||||
|     waiter: CloseWaiter<'_>, |     waiter: CloseWaiter<'_>, | ||||||
| ) -> Result<CliResponse, HandlerError> { | ) -> Result<CliResponse, HandlerError> { | ||||||
|     let state = app_handle.state::<AppState>(); |     let state = app_handle.state::<AppState>(); | ||||||
|     let credential_id = state.credential_id(&server_url).await.unwrap_or(None); |     let meta = state.docker_credential_meta(&server_url).await.unwrap_or(None); | ||||||
|     if  credential_id.is_none() { |     if  meta.is_none() { | ||||||
|         return Err( |         return Err( | ||||||
|             HandlerError::NoCredentials( |             HandlerError::NoCredentials( | ||||||
|                 GetCredentialsError::Load( |                 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?; |     let response = super::send_credentials_request(detail, app_handle.clone(), waiter).await?; | ||||||
|     match response.approval { |     match response.approval { | ||||||
|         Approval::Approved => { |         Approval::Approved => { | ||||||
| @@ -142,22 +150,45 @@ async fn get_docker_credential( | |||||||
| async fn store_docker_credential( | async fn store_docker_credential( | ||||||
|     docker_credential: DockerCredential, |     docker_credential: DockerCredential, | ||||||
|     app_handle: AppHandle, |     app_handle: AppHandle, | ||||||
|     _waiter: CloseWaiter<'_>, |     client: Client, | ||||||
|  |     waiter: CloseWaiter<'_>, | ||||||
| ) -> Result<CliResponse, HandlerError> { | ) -> Result<CliResponse, HandlerError> { | ||||||
|     let state = app_handle.state::<AppState>(); |     let state = app_handle.state::<AppState>(); | ||||||
|  |  | ||||||
|     // 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 |     let detail = RequestNotificationDetail::new_docker( | ||||||
|     // back from every `get` operation, so we have to check for an existing credential |         RequestAction::Save, | ||||||
|     let id = state.credential_id(&docker_credential.server_url) |         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 |         .await | ||||||
|         .map_err(|e| GetCredentialsError::Load(e))? |         .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 { |     let record = CredentialRecord { | ||||||
|         id, |         id, | ||||||
|         name: docker_credential.server_url.clone(), |         name, | ||||||
|         is_default: false, |         is_default: false, | ||||||
|         credential: Credential::Docker(docker_credential) |         credential: Credential::Docker(docker_credential) | ||||||
|     }; |     }; | ||||||
| @@ -169,12 +200,24 @@ async fn store_docker_credential( | |||||||
| async fn erase_docker_credential( | async fn erase_docker_credential( | ||||||
|     server_url: String, |     server_url: String, | ||||||
|     app_handle: AppHandle, |     app_handle: AppHandle, | ||||||
|     _waiter: CloseWaiter<'_> |     client: Client, | ||||||
|  |     waiter: CloseWaiter<'_> | ||||||
| ) -> Result<CliResponse, HandlerError> { | ) -> Result<CliResponse, HandlerError> { | ||||||
|     let state = app_handle.state::<AppState>(); |     let state = app_handle.state::<AppState>(); | ||||||
|  |  | ||||||
|     // eventually ask the frontend to confirm here |     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?; |             state.delete_credential_by_name(&server_url).await?; | ||||||
|             Ok(CliResponse::Empty) |             Ok(CliResponse::Empty) | ||||||
|         } |         } | ||||||
|  |         Approval::Denied => { | ||||||
|  |             Err(HandlerError::Denied) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|   | |||||||
| @@ -148,13 +148,6 @@ impl AppState { | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     pub async fn credential_id(&self, name: &str) -> Result<Option<Uuid>, 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> { |     pub async fn save_credential(&self, record: CredentialRecord) -> Result<(), SaveCredentialsError> { | ||||||
|         let session = self.app_session.read().await; |         let session = self.app_session.read().await; | ||||||
|         let crypto = session.try_get_crypto()?; |         let crypto = session.try_get_crypto()?; | ||||||
| @@ -337,6 +330,23 @@ impl AppState { | |||||||
|         Ok(k) |         Ok(k) | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     pub async fn docker_credential_meta( | ||||||
|  |         &self, server_url: &str | ||||||
|  |     ) -> Result<Option<(Uuid, String)>, 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<DockerCredential, GetCredentialsError> { |     pub async fn get_docker_credential(&self, server_url: &str) -> Result<DockerCredential, GetCredentialsError> { | ||||||
|         let app_session = self.app_session.read().await; |         let app_session = self.app_session.read().await; | ||||||
|         let crypto = app_session.try_get_crypto()?; |         let crypto = app_session.try_get_crypto()?; | ||||||
|   | |||||||
| @@ -26,6 +26,12 @@ | |||||||
|         }; |         }; | ||||||
|         dispatch('response'); |         dispatch('response'); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     const actionDescriptions = { | ||||||
|  |         Access: 'access your', | ||||||
|  |         Delete: 'delete your', | ||||||
|  |         Save: 'create new', | ||||||
|  |     }; | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -52,7 +58,7 @@ | |||||||
|         {:else if $appState.currentRequest.type === 'Ssh'} |         {:else if $appState.currentRequest.type === 'Ssh'} | ||||||
|             {appName ? `"${appName}"` : 'An application'} would like to use your SSH key "{$appState.currentRequest.key_name}". |             {appName ? `"${appName}"` : 'An application'} would like to use your SSH key "{$appState.currentRequest.key_name}". | ||||||
|         {:else if $appState.currentRequest.type === 'Docker'} |         {:else if $appState.currentRequest.type === 'Docker'} | ||||||
|             {appName ? `"${appName}"` : 'An application'} would like to use your Docker credentials for <code>{$appState.currentRequest.server_url}</code>. |             {appName ? `"${appName}"` : 'An application'} would like to {actionDescriptions[$appState.currentRequest.action]} Docker credentials for <code>{$appState.currentRequest.server_url}</code>. | ||||||
|         {/if} |         {/if} | ||||||
|     </h2> |     </h2> | ||||||
|  |  | ||||||
|   | |||||||
| @@ -83,6 +83,13 @@ | |||||||
|                     bind:value={local.credential.ServerURL} |                     bind:value={local.credential.ServerURL} | ||||||
|                 > |                 > | ||||||
|  |  | ||||||
|  |                 <span class="justify-self-end">Username</span> | ||||||
|  |                 <input | ||||||
|  |                     type="text" | ||||||
|  |                     class="input input-bordered font-mono bg-transparent" | ||||||
|  |                     bind:value={local.credential.Username} | ||||||
|  |                 > | ||||||
|  |  | ||||||
|                 <span>Password</span> |                 <span>Password</span> | ||||||
|                 <div class="font-mono"> |                 <div class="font-mono"> | ||||||
|                     <PassphraseInput class="bg-transparent" bind:value={local.credential.Secret} /> |                     <PassphraseInput class="bg-transparent" bind:value={local.credential.Secret} /> | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user