finish basic Docker credential helper implementation

This commit is contained in:
Joseph Montanaro 2024-11-25 14:47:30 -05:00
parent 479a0a96eb
commit e0e758554c
5 changed files with 103 additions and 28 deletions

View File

@ -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})
} }
} }

View File

@ -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)
}
}
}

View File

@ -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()?;

View File

@ -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>

View File

@ -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} />