finish basic Docker credential helper implementation
This commit is contained in:
parent
479a0a96eb
commit
e0e758554c
@ -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} />
|
||||||
|
Loading…
x
Reference in New Issue
Block a user