show client username, check whether credential exists before requesting confirmation from frontend

This commit is contained in:
Joseph Montanaro 2024-11-25 11:22:27 -05:00
parent 9bc9cb56c1
commit c6e22fc91b
8 changed files with 52 additions and 14 deletions

View File

@ -5,7 +5,8 @@ use sysinfo::{
SystemExt, SystemExt,
Pid, Pid,
PidExt, PidExt,
ProcessExt ProcessExt,
UserExt,
}; };
use serde::{Serialize, Deserialize}; use serde::{Serialize, Deserialize};
@ -16,13 +17,16 @@ use crate::errors::*;
pub struct Client { pub struct Client {
pub pid: u32, pub pid: u32,
pub exe: Option<PathBuf>, pub exe: Option<PathBuf>,
pub username: Option<String>,
} }
pub fn get_client(pid: u32, parent: bool) -> Result<Client, ClientInfoError> { pub fn get_client(pid: u32, parent: bool) -> Result<Client, ClientInfoError> {
let sys_pid = Pid::from_u32(pid); let sys_pid = Pid::from_u32(pid);
let mut sys = System::new(); let mut sys = System::new();
sys.refresh_process(sys_pid); sys.refresh_process(sys_pid);
sys.refresh_users_list();
let mut proc = sys.process(sys_pid) let mut proc = sys.process(sys_pid)
.ok_or(ClientInfoError::ProcessNotFound)?; .ok_or(ClientInfoError::ProcessNotFound)?;
@ -34,10 +38,15 @@ pub fn get_client(pid: u32, parent: bool) -> Result<Client, ClientInfoError> {
.ok_or(ClientInfoError::ParentProcessNotFound)?; .ok_or(ClientInfoError::ParentProcessNotFound)?;
} }
let username = proc.user_id()
.map(|uid| sys.get_user_by_id(uid))
.flatten()
.map(|u| u.name().to_owned());
let exe = match proc.exe() { let exe = match proc.exe() {
p if p == Path::new("") => None, p if p == Path::new("") => None,
p => Some(PathBuf::from(p)), p => Some(PathBuf::from(p)),
}; };
Ok(Client { pid: proc.pid().as_u32(), exe }) Ok(Client { pid: proc.pid().as_u32(), exe, username })
} }

View File

@ -139,3 +139,10 @@ pub trait PersistentCredential: for<'a> Deserialize<'a> + Sized {
Ok(creds) Ok(creds)
} }
} }
pub fn random_uuid() -> Uuid {
// a bit weird to use salt() for this, but it's convenient
let random_bytes = Crypto::salt();
Uuid::from_slice(&random_bytes[..16]).unwrap()
}

View File

@ -6,12 +6,11 @@ use ssh_agent_lib::proto::message::{
}; };
use tauri::{AppHandle, Manager}; use tauri::{AppHandle, Manager};
use tokio_stream::StreamExt; use tokio_stream::StreamExt;
use tokio::sync::oneshot;
use tokio_util::codec::Framed; use tokio_util::codec::Framed;
use crate::clientinfo; use crate::clientinfo;
use crate::errors::*; use crate::errors::*;
use crate::ipc::{Approval, RequestNotification, RequestNotificationDetail}; use crate::ipc::{Approval, RequestNotificationDetail};
use crate::state::AppState; use crate::state::AppState;
use super::{CloseWaiter, Stream}; use super::{CloseWaiter, Stream};

View File

@ -1,17 +1,17 @@
use sqlx::types::uuid::Uuid; use sqlx::types::uuid::Uuid;
use tauri::{AppHandle, Manager}; use tauri::{AppHandle, Manager};
use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::sync::oneshot;
use crate::clientinfo::{self, Client}; use crate::clientinfo::{self, Client};
use crate::credentials::{ use crate::credentials::{
self,
Credential, Credential,
CredentialRecord, CredentialRecord,
Crypto, Crypto,
DockerCredential DockerCredential,
}; };
use crate::errors::*; use crate::errors::*;
use crate::ipc::{Approval, AwsRequestNotification, RequestNotificationDetail, RequestResponse}; use crate::ipc::{Approval, RequestNotificationDetail};
use crate::shortcuts::{self, ShortcutAction}; use crate::shortcuts::{self, ShortcutAction};
use crate::state::AppState; use crate::state::AppState;
use super::{ use super::{
@ -116,11 +116,22 @@ async fn get_docker_credential(
app_handle: AppHandle, app_handle: AppHandle,
waiter: CloseWaiter<'_>, waiter: CloseWaiter<'_>,
) -> Result<CliResponse, HandlerError> { ) -> Result<CliResponse, HandlerError> {
let state = app_handle.state::<AppState>();
let credential_id = state.credential_id(&server_url).await.unwrap_or(None);
if credential_id.is_none() {
return Err(
HandlerError::NoCredentials(
GetCredentialsError::Load(
LoadCredentialsError::NoCredentials
)
)
);
}
let detail = RequestNotificationDetail::new_docker(client, server_url.clone()); let detail = RequestNotificationDetail::new_docker(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 => {
let state = app_handle.state::<AppState>();
let creds = state.get_docker_credential(&server_url).await?; let creds = state.get_docker_credential(&server_url).await?;
Ok(CliResponse::Credential(CliCredential::Docker(creds))) Ok(CliResponse::Credential(CliCredential::Docker(creds)))
}, },
@ -139,9 +150,12 @@ async fn store_docker_credential(
// eventually ask the frontend to confirm here // eventually ask the frontend to confirm here
// a bit weird but convenient // for some reason Docker likes to call `store` immediately with whatever it gets
let random_bytes = Crypto::salt(); // back from every `get` operation, so we have to check for an existing credential
let id = Uuid::from_slice(&random_bytes[..16]).unwrap(); let id = state.credential_id(&docker_credential.server_url)
.await
.map_err(|e| GetCredentialsError::Load(e))?
.unwrap_or_else(|| credentials::random_uuid());
let record = CredentialRecord { let record = CredentialRecord {
id, id,

View File

@ -9,7 +9,6 @@ use tokio::io::AsyncReadExt;
use tokio::sync::oneshot; use tokio::sync::oneshot;
use serde::{Serialize, Deserialize}; use serde::{Serialize, Deserialize};
use crate::clientinfo::Client;
use crate::credentials::{ use crate::credentials::{
AwsBaseCredential, AwsBaseCredential,
AwsSessionCredential, AwsSessionCredential,

View File

@ -148,6 +148,13 @@ 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()?;

View File

@ -7,6 +7,7 @@
import ShowResponse from './approve/ShowResponse.svelte'; import ShowResponse from './approve/ShowResponse.svelte';
import Unlock from './Unlock.svelte'; import Unlock from './Unlock.svelte';
console.log($appState.currentRequest);
// Extra 50ms so the window can finish disappearing before the redraw // Extra 50ms so the window can finish disappearing before the redraw
const rehideDelay = Math.min(5000, $appState.config.rehide_ms + 100); const rehideDelay = Math.min(5000, $appState.config.rehide_ms + 100);

View File

@ -14,7 +14,7 @@
// Extract executable name from full path // Extract executable name from full path
const client = $appState.currentRequest.client; const client = $appState.currentRequest.client;
const m = client.exe?.match(/\/([^/]+?$)|\\([^\\]+?$)/); const m = client.exe?.match(/\/([^/]+?$)|\\([^\\]+?$)/);
const appName = m[1] || m[2]; const appName = m ? m[1] || m[2] : '';
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
@ -61,6 +61,8 @@
<code class="">{@html client.exe ? breakPath(client.exe) : 'Unknown'}</code> <code class="">{@html client.exe ? breakPath(client.exe) : 'Unknown'}</code>
<div class="text-right">PID:</div> <div class="text-right">PID:</div>
<code>{client.pid}</code> <code>{client.pid}</code>
<div class="text-right">User:</div>
<code>{client.username ?? 'Unknown'}</code>
</div> </div>
</div> </div>