Compare commits
8 Commits
Author | SHA1 | Date | |
---|---|---|---|
1df849442e | |||
7fdb336c79 | |||
46b8d810c5 | |||
dd40eb379e | |||
13545ac725 | |||
040a01536a | |||
4e2a90b15b | |||
e0d919ed4a |
@ -1,8 +1,9 @@
|
|||||||
## Definitely
|
## Definitely
|
||||||
|
|
||||||
* Switch to "process" provider for AWS credentials (much less hacky)
|
* ~~Switch to "process" provider for AWS credentials (much less hacky)~~
|
||||||
|
* ~~Frontend needs to react when request is cancelled from backend~~
|
||||||
* Session timeout (plain duration, or activity-based?)
|
* Session timeout (plain duration, or activity-based?)
|
||||||
* ~Fix rehide behavior when new request comes in while old one is still being resolved~
|
* ~~Fix rehide behavior when new request comes in while old one is still being resolved~~
|
||||||
* Additional hotkey configuration (approve/deny at the very least)
|
* Additional hotkey configuration (approve/deny at the very least)
|
||||||
* Logging
|
* Logging
|
||||||
* Icon
|
* Icon
|
||||||
|
1854
package-lock.json
generated
1854
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "creddy",
|
"name": "creddy",
|
||||||
"version": "0.4.0",
|
"version": "0.4.4",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
|
2
src-tauri/Cargo.lock
generated
2
src-tauri/Cargo.lock
generated
@ -1035,7 +1035,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "creddy"
|
name = "creddy"
|
||||||
version = "0.4.0"
|
version = "0.4.4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"argon2",
|
"argon2",
|
||||||
"auto-launch",
|
"auto-launch",
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "creddy"
|
name = "creddy"
|
||||||
version = "0.4.0"
|
version = "0.4.4"
|
||||||
description = "A friendly AWS credentials manager"
|
description = "A friendly AWS credentials manager"
|
||||||
authors = ["Joseph Montanaro"]
|
authors = ["Joseph Montanaro"]
|
||||||
license = ""
|
license = ""
|
||||||
|
@ -108,6 +108,10 @@ async fn setup(app: &mut App) -> Result<(), Box<dyn Error>> {
|
|||||||
setup_errors.push("Failed to register hotkeys. Hotkey settings have been disabled.".into());
|
setup_errors.push("Failed to register hotkeys. Hotkey settings have been disabled.".into());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let desktop_is_gnome = std::env::var("XDG_CURRENT_DESKTOP")
|
||||||
|
.map(|names| names.split(':').any(|n| n == "GNOME"))
|
||||||
|
.unwrap_or(false);
|
||||||
|
|
||||||
// if session is empty, this is probably the first launch, so don't autohide
|
// if session is empty, this is probably the first launch, so don't autohide
|
||||||
if !conf.start_minimized || is_first_launch {
|
if !conf.start_minimized || is_first_launch {
|
||||||
app.get_window("main")
|
app.get_window("main")
|
||||||
@ -115,7 +119,7 @@ async fn setup(app: &mut App) -> Result<(), Box<dyn Error>> {
|
|||||||
.show()?;
|
.show()?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let state = AppState::new(conf, session, pool, setup_errors);
|
let state = AppState::new(conf, session, pool, setup_errors, desktop_is_gnome);
|
||||||
app.manage(state);
|
app.manage(state);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -14,7 +14,6 @@ pub struct Client {
|
|||||||
|
|
||||||
|
|
||||||
pub fn get_process_parent_info(pid: u32) -> Result<Client, ClientInfoError> {
|
pub fn get_process_parent_info(pid: u32) -> Result<Client, ClientInfoError> {
|
||||||
dbg!(pid);
|
|
||||||
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);
|
||||||
@ -34,59 +33,3 @@ pub fn get_process_parent_info(pid: u32) -> Result<Client, ClientInfoError> {
|
|||||||
|
|
||||||
Ok(Client { pid: parent_pid_sys.as_u32(), exe })
|
Ok(Client { pid: parent_pid_sys.as_u32(), exe })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// async fn get_associated_pids(local_port: u16) -> Result<Vec<u32>, netstat2::error::Error> {
|
|
||||||
// let state = APP.get().unwrap().state::<AppState>();
|
|
||||||
// let AppConfig {
|
|
||||||
// listen_addr: app_listen_addr,
|
|
||||||
// listen_port: app_listen_port,
|
|
||||||
// ..
|
|
||||||
// } = *state.config.read().await;
|
|
||||||
|
|
||||||
// let sockets_iter = netstat2::iterate_sockets_info(
|
|
||||||
// AddressFamilyFlags::IPV4,
|
|
||||||
// ProtocolFlags::TCP
|
|
||||||
// )?;
|
|
||||||
// for item in sockets_iter {
|
|
||||||
// let sock_info = item?;
|
|
||||||
// let proto_info = match sock_info.protocol_socket_info {
|
|
||||||
// ProtocolSocketInfo::Tcp(tcp_info) => tcp_info,
|
|
||||||
// ProtocolSocketInfo::Udp(_) => {continue;}
|
|
||||||
// };
|
|
||||||
|
|
||||||
// if proto_info.local_port == local_port
|
|
||||||
// && proto_info.remote_port == app_listen_port
|
|
||||||
// && proto_info.local_addr == app_listen_addr
|
|
||||||
// && proto_info.remote_addr == app_listen_addr
|
|
||||||
// {
|
|
||||||
// return Ok(sock_info.associated_pids)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// Ok(vec![])
|
|
||||||
// }
|
|
||||||
|
|
||||||
|
|
||||||
// Theoretically, on some systems, multiple processes can share a socket
|
|
||||||
// pub async fn get_clients(local_port: u16) -> Result<Vec<Option<Client>>, ClientInfoError> {
|
|
||||||
// let mut clients = Vec::new();
|
|
||||||
// let mut sys = System::new();
|
|
||||||
// for p in get_associated_pids(local_port).await? {
|
|
||||||
// let pid = Pid::from_u32(p);
|
|
||||||
// sys.refresh_process(pid);
|
|
||||||
// let proc = sys.process(pid)
|
|
||||||
// .ok_or(ClientInfoError::ProcessNotFound)?;
|
|
||||||
|
|
||||||
// let client = Client {
|
|
||||||
// pid: p,
|
|
||||||
// exe: proc.exe().to_path_buf(),
|
|
||||||
// };
|
|
||||||
// clients.push(Some(client));
|
|
||||||
// }
|
|
||||||
|
|
||||||
// if clients.is_empty() {
|
|
||||||
// clients.push(None);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// Ok(clients)
|
|
||||||
// }
|
|
||||||
|
@ -18,6 +18,7 @@ use tauri::api::dialog::{
|
|||||||
MessageDialogBuilder,
|
MessageDialogBuilder,
|
||||||
MessageDialogKind,
|
MessageDialogKind,
|
||||||
};
|
};
|
||||||
|
use tokio::sync::oneshot::error::RecvError;
|
||||||
use serde::{
|
use serde::{
|
||||||
Serialize,
|
Serialize,
|
||||||
Serializer,
|
Serializer,
|
||||||
@ -82,15 +83,33 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
struct SerializeUpstream<E>(pub E);
|
||||||
|
|
||||||
|
impl<E: Error> Serialize for SerializeUpstream<E> {
|
||||||
|
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
||||||
|
let msg = format!("{}", self.0);
|
||||||
|
let mut map = serializer.serialize_map(None)?;
|
||||||
|
map.serialize_entry("msg", &msg)?;
|
||||||
|
map.serialize_entry("code", &None::<&str>)?;
|
||||||
|
map.serialize_entry("source", &None::<&str>)?;
|
||||||
|
map.end()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn serialize_upstream_err<E, M>(err: &E, map: &mut M) -> Result<(), M::Error>
|
fn serialize_upstream_err<E, M>(err: &E, map: &mut M) -> Result<(), M::Error>
|
||||||
where
|
where
|
||||||
E: Error,
|
E: Error,
|
||||||
M: serde::ser::SerializeMap,
|
M: serde::ser::SerializeMap,
|
||||||
{
|
{
|
||||||
let msg = err.source().map(|s| format!("{s}"));
|
// let msg = err.source().map(|s| format!("{s}"));
|
||||||
map.serialize_entry("msg", &msg)?;
|
// map.serialize_entry("msg", &msg)?;
|
||||||
map.serialize_entry("code", &None::<&str>)?;
|
// map.serialize_entry("code", &None::<&str>)?;
|
||||||
map.serialize_entry("source", &None::<&str>)?;
|
// map.serialize_entry("source", &None::<&str>)?;
|
||||||
|
|
||||||
|
match err.source() {
|
||||||
|
Some(src) => map.serialize_entry("source", &SerializeUpstream(src))?,
|
||||||
|
None => map.serialize_entry("source", &None::<&str>)?,
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@ -152,7 +171,7 @@ pub enum SendResponseError {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// errors encountered while handling an HTTP request
|
// errors encountered while handling a client request
|
||||||
#[derive(Debug, ThisError, AsRefStr)]
|
#[derive(Debug, ThisError, AsRefStr)]
|
||||||
pub enum HandlerError {
|
pub enum HandlerError {
|
||||||
#[error("Error writing to stream: {0}")]
|
#[error("Error writing to stream: {0}")]
|
||||||
@ -163,8 +182,10 @@ pub enum HandlerError {
|
|||||||
BadRequest(#[from] serde_json::Error),
|
BadRequest(#[from] serde_json::Error),
|
||||||
#[error("HTTP request too large")]
|
#[error("HTTP request too large")]
|
||||||
RequestTooLarge,
|
RequestTooLarge,
|
||||||
|
#[error("Connection closed early by client")]
|
||||||
|
Abandoned,
|
||||||
#[error("Internal server error")]
|
#[error("Internal server error")]
|
||||||
Internal,
|
Internal(#[from] RecvError),
|
||||||
#[error("Error accessing credentials: {0}")]
|
#[error("Error accessing credentials: {0}")]
|
||||||
NoCredentials(#[from] GetCredentialsError),
|
NoCredentials(#[from] GetCredentialsError),
|
||||||
#[error("Error getting client details: {0}")]
|
#[error("Error getting client details: {0}")]
|
||||||
@ -344,7 +365,6 @@ impl Serialize for SerializeWrapper<&GetSessionTokenError> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
impl_serialize_basic!(SetupError);
|
impl_serialize_basic!(SetupError);
|
||||||
impl_serialize_basic!(GetCredentialsError);
|
impl_serialize_basic!(GetCredentialsError);
|
||||||
impl_serialize_basic!(ClientInfoError);
|
impl_serialize_basic!(ClientInfoError);
|
||||||
|
@ -21,6 +21,7 @@ pub struct AwsRequestNotification {
|
|||||||
pub struct RequestResponse {
|
pub struct RequestResponse {
|
||||||
pub id: u64,
|
pub id: u64,
|
||||||
pub approval: Approval,
|
pub approval: Approval,
|
||||||
|
pub base: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -43,6 +43,24 @@ pub enum Response {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
struct CloseWaiter<'s> {
|
||||||
|
stream: &'s mut Stream,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'s> CloseWaiter<'s> {
|
||||||
|
async fn wait_for_close(&mut self) -> std::io::Result<()> {
|
||||||
|
let mut buf = [0u8; 8];
|
||||||
|
loop {
|
||||||
|
match self.stream.read(&mut buf).await {
|
||||||
|
Ok(0) => break Ok(()),
|
||||||
|
Ok(_) => (),
|
||||||
|
Err(e) => break Err(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
async fn handle(mut stream: Stream, app_handle: AppHandle, client_pid: u32) -> Result<(), HandlerError>
|
async fn handle(mut stream: Stream, app_handle: AppHandle, client_pid: u32) -> Result<(), HandlerError>
|
||||||
{
|
{
|
||||||
// read from stream until delimiter is reached
|
// read from stream until delimiter is reached
|
||||||
@ -59,13 +77,21 @@ async fn handle(mut stream: Stream, app_handle: AppHandle, client_pid: u32) -> R
|
|||||||
}
|
}
|
||||||
|
|
||||||
let client = clientinfo::get_process_parent_info(client_pid)?;
|
let client = clientinfo::get_process_parent_info(client_pid)?;
|
||||||
|
let waiter = CloseWaiter { stream: &mut stream };
|
||||||
|
|
||||||
let req: Request = serde_json::from_slice(&buf)?;
|
let req: Request = serde_json::from_slice(&buf)?;
|
||||||
let res = match req {
|
let res = match req {
|
||||||
Request::GetAwsCredentials{ base } => get_aws_credentials(base, client, app_handle).await,
|
Request::GetAwsCredentials{ base } => get_aws_credentials(
|
||||||
|
base, client, app_handle, waiter
|
||||||
|
).await,
|
||||||
Request::InvokeShortcut(action) => invoke_shortcut(action).await,
|
Request::InvokeShortcut(action) => invoke_shortcut(action).await,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// doesn't make sense to send the error to the client if the client has already left
|
||||||
|
if let Err(HandlerError::Abandoned) = res {
|
||||||
|
return Err(HandlerError::Abandoned);
|
||||||
|
}
|
||||||
|
|
||||||
let res = serde_json::to_vec(&res).unwrap();
|
let res = serde_json::to_vec(&res).unwrap();
|
||||||
stream.write_all(&res).await?;
|
stream.write_all(&res).await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -78,7 +104,12 @@ async fn invoke_shortcut(action: ShortcutAction) -> Result<Response, HandlerErro
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async fn get_aws_credentials(base: bool, client: Client, app_handle: AppHandle) -> Result<Response, HandlerError> {
|
async fn get_aws_credentials(
|
||||||
|
base: bool,
|
||||||
|
client: Client,
|
||||||
|
app_handle: AppHandle,
|
||||||
|
mut waiter: CloseWaiter<'_>,
|
||||||
|
) -> Result<Response, HandlerError> {
|
||||||
let state = app_handle.state::<AppState>();
|
let state = app_handle.state::<AppState>();
|
||||||
let rehide_ms = {
|
let rehide_ms = {
|
||||||
let config = state.config.read().await;
|
let config = state.config.read().await;
|
||||||
@ -97,9 +128,17 @@ async fn get_aws_credentials(base: bool, client: Client, app_handle: AppHandle)
|
|||||||
let notification = AwsRequestNotification {id: request_id, client, base};
|
let notification = AwsRequestNotification {id: request_id, client, base};
|
||||||
app_handle.emit_all("credentials-request", ¬ification)?;
|
app_handle.emit_all("credentials-request", ¬ification)?;
|
||||||
|
|
||||||
match chan_recv.await {
|
let response = tokio::select! {
|
||||||
Ok(Approval::Approved) => {
|
r = chan_recv => r?,
|
||||||
if base {
|
_ = waiter.wait_for_close() => {
|
||||||
|
app_handle.emit_all("request-cancelled", request_id)?;
|
||||||
|
return Err(HandlerError::Abandoned);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
match response.approval {
|
||||||
|
Approval::Approved => {
|
||||||
|
if response.base {
|
||||||
let creds = state.base_creds_cloned().await?;
|
let creds = state.base_creds_cloned().await?;
|
||||||
Ok(Response::Aws(Credentials::Base(creds)))
|
Ok(Response::Aws(Credentials::Base(creds)))
|
||||||
}
|
}
|
||||||
@ -108,8 +147,7 @@ async fn get_aws_credentials(base: bool, client: Client, app_handle: AppHandle)
|
|||||||
Ok(Response::Aws(Credentials::Session(creds)))
|
Ok(Response::Aws(Credentials::Session(creds)))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Ok(Approval::Denied) => Err(HandlerError::Denied),
|
Approval::Denied => Err(HandlerError::Denied),
|
||||||
Err(_e) => Err(HandlerError::Internal),
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,11 +1,10 @@
|
|||||||
use tokio::{
|
use tokio::net::windows::named_pipe::{
|
||||||
net::windows::named_pipe::{
|
NamedPipeServer,
|
||||||
NamedPipeServer,
|
ServerOptions,
|
||||||
ServerOptions,
|
|
||||||
},
|
|
||||||
sync::oneshot,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use tauri::{AppHandle, Manager};
|
||||||
|
|
||||||
use windows::Win32:: {
|
use windows::Win32:: {
|
||||||
Foundation::HANDLE,
|
Foundation::HANDLE,
|
||||||
System::Pipes::GetNamedPipeClientProcessId,
|
System::Pipes::GetNamedPipeClientProcessId,
|
||||||
@ -52,11 +51,11 @@ impl Server {
|
|||||||
|
|
||||||
// create a new pipe instance to listen for the next client, and swap it in
|
// create a new pipe instance to listen for the next client, and swap it in
|
||||||
let new_listener = ServerOptions::new().create(r"\\.\pipe\creddy-requests")?;
|
let new_listener = ServerOptions::new().create(r"\\.\pipe\creddy-requests")?;
|
||||||
let mut stream = std::mem::replace(&mut self.listener, new_listener);
|
let stream = std::mem::replace(&mut self.listener, new_listener);
|
||||||
let new_handle = self.app_handle.app_handle();
|
let new_handle = self.app_handle.app_handle();
|
||||||
let client_pid = get_client_pid(&stream)?;
|
let client_pid = get_client_pid(&stream)?;
|
||||||
rt::spawn(async move {
|
rt::spawn(async move {
|
||||||
super::handle(stream, app_handle)
|
super::handle(stream, new_handle, client_pid)
|
||||||
.await
|
.await
|
||||||
.error_print_prefix("Error responding to request: ");
|
.error_print_prefix("Error responding to request: ");
|
||||||
});
|
});
|
||||||
@ -71,5 +70,5 @@ fn get_client_pid(pipe: &NamedPipeServer) -> Result<u32, ClientInfoError> {
|
|||||||
let mut pid = 0u32;
|
let mut pid = 0u32;
|
||||||
let handle = HANDLE(raw_handle as _);
|
let handle = HANDLE(raw_handle as _);
|
||||||
unsafe { GetNamedPipeClientProcessId(handle, &mut pid as *mut u32)? };
|
unsafe { GetNamedPipeClientProcessId(handle, &mut pid as *mut u32)? };
|
||||||
pid
|
Ok(pid)
|
||||||
}
|
}
|
||||||
|
@ -17,7 +17,7 @@ use crate::credentials::{
|
|||||||
SessionCredentials,
|
SessionCredentials,
|
||||||
};
|
};
|
||||||
use crate::{config, config::AppConfig};
|
use crate::{config, config::AppConfig};
|
||||||
use crate::ipc::{self, Approval};
|
use crate::ipc::{self, Approval, RequestResponse};
|
||||||
use crate::errors::*;
|
use crate::errors::*;
|
||||||
use crate::shortcuts;
|
use crate::shortcuts;
|
||||||
|
|
||||||
@ -39,13 +39,22 @@ impl Visibility {
|
|||||||
.ok_or(WindowError::NoMainWindow)?;
|
.ok_or(WindowError::NoMainWindow)?;
|
||||||
|
|
||||||
self.leases += 1;
|
self.leases += 1;
|
||||||
|
// `original` represents the visibility of the window before any leases were acquired
|
||||||
|
// None means we don't know, Some(false) means it was previously hidden,
|
||||||
|
// Some(true) means it was previously visible
|
||||||
if self.original.is_none() {
|
if self.original.is_none() {
|
||||||
let is_visible = window.is_visible()?;
|
let is_visible = window.is_visible()?;
|
||||||
self.original = Some(is_visible);
|
self.original = Some(is_visible);
|
||||||
if !is_visible {
|
|
||||||
window.show()?;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let state = app.state::<AppState>();
|
||||||
|
if matches!(self.original, Some(true)) && state.desktop_is_gnome {
|
||||||
|
// Gnome has a really annoying "focus-stealing prevention" behavior means we
|
||||||
|
// can't just pop up when the window is already visible, so to work around it
|
||||||
|
// we hide and then immediately unhide the window
|
||||||
|
window.hide()?;
|
||||||
|
}
|
||||||
|
window.show()?;
|
||||||
window.set_focus()?;
|
window.set_focus()?;
|
||||||
|
|
||||||
let (tx, rx) = oneshot::channel();
|
let (tx, rx) = oneshot::channel();
|
||||||
@ -93,10 +102,11 @@ pub struct AppState {
|
|||||||
pub config: RwLock<AppConfig>,
|
pub config: RwLock<AppConfig>,
|
||||||
pub session: RwLock<Session>,
|
pub session: RwLock<Session>,
|
||||||
pub request_count: RwLock<u64>,
|
pub request_count: RwLock<u64>,
|
||||||
pub waiting_requests: RwLock<HashMap<u64, Sender<Approval>>>,
|
pub waiting_requests: RwLock<HashMap<u64, Sender<RequestResponse>>>,
|
||||||
pub pending_terminal_request: RwLock<bool>,
|
pub pending_terminal_request: RwLock<bool>,
|
||||||
// setup_errors is never modified and so doesn't need to be wrapped in RwLock
|
// these are never modified and so don't need to be wrapped in RwLocks
|
||||||
pub setup_errors: Vec<String>,
|
pub setup_errors: Vec<String>,
|
||||||
|
pub desktop_is_gnome: bool,
|
||||||
pool: sqlx::SqlitePool,
|
pool: sqlx::SqlitePool,
|
||||||
visibility: RwLock<Visibility>,
|
visibility: RwLock<Visibility>,
|
||||||
}
|
}
|
||||||
@ -107,6 +117,7 @@ impl AppState {
|
|||||||
session: Session,
|
session: Session,
|
||||||
pool: SqlitePool,
|
pool: SqlitePool,
|
||||||
setup_errors: Vec<String>,
|
setup_errors: Vec<String>,
|
||||||
|
desktop_is_gnome: bool,
|
||||||
) -> AppState {
|
) -> AppState {
|
||||||
AppState {
|
AppState {
|
||||||
config: RwLock::new(config),
|
config: RwLock::new(config),
|
||||||
@ -115,6 +126,7 @@ impl AppState {
|
|||||||
waiting_requests: RwLock::new(HashMap::new()),
|
waiting_requests: RwLock::new(HashMap::new()),
|
||||||
pending_terminal_request: RwLock::new(false),
|
pending_terminal_request: RwLock::new(false),
|
||||||
setup_errors,
|
setup_errors,
|
||||||
|
desktop_is_gnome,
|
||||||
pool,
|
pool,
|
||||||
visibility: RwLock::new(Visibility::new()),
|
visibility: RwLock::new(Visibility::new()),
|
||||||
}
|
}
|
||||||
@ -149,7 +161,7 @@ impl AppState {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn register_request(&self, sender: Sender<Approval>) -> u64 {
|
pub async fn register_request(&self, sender: Sender<RequestResponse>) -> u64 {
|
||||||
let count = {
|
let count = {
|
||||||
let mut c = self.request_count.write().await;
|
let mut c = self.request_count.write().await;
|
||||||
*c += 1;
|
*c += 1;
|
||||||
@ -181,7 +193,7 @@ impl AppState {
|
|||||||
waiting_requests
|
waiting_requests
|
||||||
.remove(&response.id)
|
.remove(&response.id)
|
||||||
.ok_or(SendResponseError::NotFound)?
|
.ok_or(SendResponseError::NotFound)?
|
||||||
.send(response.approval)
|
.send(response)
|
||||||
.map_err(|_| SendResponseError::Abandoned)
|
.map_err(|_| SendResponseError::Abandoned)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
},
|
},
|
||||||
"package": {
|
"package": {
|
||||||
"productName": "creddy",
|
"productName": "creddy",
|
||||||
"version": "0.4.0"
|
"version": "0.4.4"
|
||||||
},
|
},
|
||||||
"tauri": {
|
"tauri": {
|
||||||
"allowlist": {
|
"allowlist": {
|
||||||
|
@ -3,7 +3,7 @@ import { onMount } from 'svelte';
|
|||||||
import { listen } from '@tauri-apps/api/event';
|
import { listen } from '@tauri-apps/api/event';
|
||||||
import { invoke } from '@tauri-apps/api/tauri';
|
import { invoke } from '@tauri-apps/api/tauri';
|
||||||
|
|
||||||
import { appState, acceptRequest } from './lib/state.js';
|
import { appState, acceptRequest, cleanupRequest } from './lib/state.js';
|
||||||
import { views, currentView, navigate } from './lib/routing.js';
|
import { views, currentView, navigate } from './lib/routing.js';
|
||||||
|
|
||||||
|
|
||||||
@ -16,6 +16,16 @@ listen('credentials-request', (tauriEvent) => {
|
|||||||
$appState.pendingRequests.put(tauriEvent.payload);
|
$appState.pendingRequests.put(tauriEvent.payload);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
listen('request-cancelled', (tauriEvent) => {
|
||||||
|
const id = tauriEvent.payload;
|
||||||
|
if (id === $appState.currentRequest?.id) {
|
||||||
|
cleanupRequest()
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const found = $appState.pendingRequests.find_remove(r => r.id === id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
listen('launch-terminal-request', async (tauriEvent) => {
|
listen('launch-terminal-request', async (tauriEvent) => {
|
||||||
if ($appState.currentRequest === null) {
|
if ($appState.currentRequest === null) {
|
||||||
let status = await invoke('get_session_status');
|
let status = await invoke('get_session_status');
|
||||||
|
@ -30,5 +30,15 @@ export default function() {
|
|||||||
|
|
||||||
return this.items.shift();
|
return this.items.shift();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
find_remove(pred) {
|
||||||
|
for (let i=0; i<this.items.length; i++) {
|
||||||
|
if (pred(this.items[i])) {
|
||||||
|
this.items.splice(i, 1);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -23,7 +23,7 @@ export async function acceptRequest() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export function completeRequest() {
|
export function cleanupRequest() {
|
||||||
appState.update($appState => {
|
appState.update($appState => {
|
||||||
$appState.currentRequest = null;
|
$appState.currentRequest = null;
|
||||||
return $appState;
|
return $appState;
|
||||||
|
@ -5,3 +5,8 @@
|
|||||||
.btn-alert-error {
|
.btn-alert-error {
|
||||||
@apply bg-transparent hover:bg-[#cd5a5a] border border-error-content text-error-content
|
@apply bg-transparent hover:bg-[#cd5a5a] border border-error-content text-error-content
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* I like alert icons to be top-aligned */
|
||||||
|
.alert > :where(*) {
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
@ -1,13 +1,15 @@
|
|||||||
<script>
|
<script>
|
||||||
export let keys;
|
export let keys;
|
||||||
|
let classes;
|
||||||
|
export {classes as class};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
<div class="flex gap-x-[0.2em] items-center">
|
<span class="inline-flex gap-x-[0.2em] items-center {classes}">
|
||||||
{#each keys as key, i}
|
{#each keys as key, i}
|
||||||
{#if i > 0}
|
{#if i > 0}
|
||||||
<span class="mt-[-0.1em]">+</span>
|
<span class="mt-[-0.1em]">+</span>
|
||||||
{/if}
|
{/if}
|
||||||
<kbd class="normal-case px-1 py-0.5 rounded border border-neutral">{key}</kbd>
|
<kbd class="normal-case px-1 py-0.5 rounded border border-neutral">{key}</kbd>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</span>
|
||||||
|
@ -21,15 +21,15 @@
|
|||||||
throw(`Link target is not a string or a function: ${target}`)
|
throw(`Link target is not a string or a function: ${target}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function handleHotkey(event) {
|
function handleHotkey(event) {
|
||||||
if (!hotkey) return;
|
if (
|
||||||
if (ctrl && !event.ctrlKey) return;
|
hotkey === event.key
|
||||||
if (alt && !event.altKey) return;
|
&& ctrl === event.ctrlKey
|
||||||
if (shift && !event.shiftKey) return;
|
&& alt === event.altKey
|
||||||
|
&& shift === event.shiftKey
|
||||||
if (event.key === hotkey) {
|
) {
|
||||||
click();
|
click();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
import { invoke } from '@tauri-apps/api/tauri';
|
import { invoke } from '@tauri-apps/api/tauri';
|
||||||
|
|
||||||
import { navigate } from '../lib/routing.js';
|
import { navigate } from '../lib/routing.js';
|
||||||
import { appState, completeRequest } from '../lib/state.js';
|
import { appState, cleanupRequest } from '../lib/state.js';
|
||||||
import ErrorAlert from '../ui/ErrorAlert.svelte';
|
import ErrorAlert from '../ui/ErrorAlert.svelte';
|
||||||
import Link from '../ui/Link.svelte';
|
import Link from '../ui/Link.svelte';
|
||||||
import KeyCombo from '../ui/KeyCombo.svelte';
|
import KeyCombo from '../ui/KeyCombo.svelte';
|
||||||
@ -12,9 +12,12 @@
|
|||||||
// Send response to backend, display error if applicable
|
// Send response to backend, display error if applicable
|
||||||
let error, alert;
|
let error, alert;
|
||||||
async function respond() {
|
async function respond() {
|
||||||
let {id, approval} = $appState.currentRequest;
|
const response = {
|
||||||
|
id: $appState.currentRequest.id,
|
||||||
|
...$appState.currentRequest.response,
|
||||||
|
};
|
||||||
try {
|
try {
|
||||||
await invoke('respond', {response: {id, approval}});
|
await invoke('respond', {response});
|
||||||
navigate('ShowResponse');
|
navigate('ShowResponse');
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch (e) {
|
||||||
@ -26,8 +29,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Approval has one of several outcomes depending on current credential state
|
// Approval has one of several outcomes depending on current credential state
|
||||||
async function approve() {
|
async function approve(base) {
|
||||||
$appState.currentRequest.approval = 'Approved';
|
$appState.currentRequest.response = {approval: 'Approved', base};
|
||||||
let status = await invoke('get_session_status');
|
let status = await invoke('get_session_status');
|
||||||
if (status === 'unlocked') {
|
if (status === 'unlocked') {
|
||||||
await respond();
|
await respond();
|
||||||
@ -40,9 +43,16 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function approve_base() {
|
||||||
|
approve(true);
|
||||||
|
}
|
||||||
|
function approve_session() {
|
||||||
|
approve(false);
|
||||||
|
}
|
||||||
|
|
||||||
// Denial has only one
|
// Denial has only one
|
||||||
async function deny() {
|
async function deny() {
|
||||||
$appState.currentRequest.approval = 'Denied';
|
$appState.currentRequest.response = {approval: 'Denied', base: false};
|
||||||
await respond();
|
await respond();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -58,32 +68,32 @@
|
|||||||
|
|
||||||
// if the request has already been approved/denied, send response immediately
|
// if the request has already been approved/denied, send response immediately
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
if ($appState.currentRequest.approval) {
|
if ($appState.currentRequest.response) {
|
||||||
await respond();
|
await respond();
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
<!-- Don't render at all if we're just going to immediately proceed to the next screen -->
|
<!-- Don't render at all if we're just going to immediately proceed to the next screen -->
|
||||||
{#if error || !$appState.currentRequest.approval}
|
{#if error || !$appState.currentRequest?.response}
|
||||||
<div class="flex flex-col space-y-4 p-4 m-auto max-w-xl h-screen items-center justify-center">
|
<div class="flex flex-col space-y-4 p-4 m-auto max-w-xl h-screen items-center justify-center">
|
||||||
{#if error}
|
{#if error}
|
||||||
<ErrorAlert bind:this={alert}>
|
<ErrorAlert bind:this={alert}>
|
||||||
{error}
|
{error.msg}
|
||||||
<svelte:fragment slot="buttons">
|
<svelte:fragment slot="buttons">
|
||||||
<button class="btn btn-sm btn-alert-error" on:click={completeRequest}>Cancel</button>
|
<button class="btn btn-sm btn-alert-error" on:click={cleanupRequest}>Cancel</button>
|
||||||
<button class="btn btn-sm btn-alert-error" on:click={respond}>Retry</button>
|
<button class="btn btn-sm btn-alert-error" on:click={respond}>Retry</button>
|
||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
</ErrorAlert>
|
</ErrorAlert>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if $appState.currentRequest.base}
|
{#if $appState.currentRequest?.base}
|
||||||
<div class="alert alert-warning shadow-lg">
|
<div class="alert alert-warning shadow-lg">
|
||||||
<div>
|
<div>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current flex-shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" /></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current flex-shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" /></svg>
|
||||||
<span>
|
<span>
|
||||||
WARNING: This application is requesting your base (long-lived) AWS credentials.
|
WARNING: This application is requesting your base AWS credentials.
|
||||||
These credentials are less secure than session credentials, since they don't expire automatically.
|
These credentials are less secure than session credentials, since they don't expire automatically.
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@ -101,20 +111,42 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="w-full flex justify-between">
|
<div class="w-full grid grid-cols-[1fr_auto] items-center gap-y-6">
|
||||||
<Link target={deny} hotkey="Escape">
|
<!-- Don't display the option to approve with session credentials if base was specifically requested -->
|
||||||
<button class="btn btn-error justify-self-start">
|
{#if !$appState.currentRequest?.base}
|
||||||
<span class="mr-2">Deny</span>
|
<h3 class="font-semibold">
|
||||||
<KeyCombo keys={['Esc']} />
|
Approve with session credentials
|
||||||
</button>
|
</h3>
|
||||||
</Link>
|
<Link target={() => approve(false)} hotkey="Enter" shift={true}>
|
||||||
|
<button class="w-full btn btn-success">
|
||||||
|
<KeyCombo keys={['Shift', 'Enter']} />
|
||||||
|
</button>
|
||||||
|
</Link>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<Link target={approve} hotkey="Enter" shift="{true}">
|
<h3 class="font-semibold">
|
||||||
<button class="btn btn-success justify-self-end">
|
<span class="mr-2">
|
||||||
<span class="mr-2">Approve</span>
|
{#if $appState.currentRequest?.base}
|
||||||
<KeyCombo keys={['Shift', 'Enter']} />
|
Approve
|
||||||
</button>
|
{:else}
|
||||||
</Link>
|
Approve with base credentials
|
||||||
|
{/if}
|
||||||
|
</span>
|
||||||
|
</h3>
|
||||||
|
<Link target={() => approve(true)} hotkey="Enter" shift={true} ctrl={true}>
|
||||||
|
<button class="w-full btn btn-warning">
|
||||||
|
<KeyCombo keys={['Ctrl', 'Shift', 'Enter']} />
|
||||||
|
</button>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<h3 class="font-semibold">
|
||||||
|
<span class="mr-2">Deny</span>
|
||||||
|
</h3>
|
||||||
|
<Link target={deny} hotkey="Escape">
|
||||||
|
<button class="w-full btn btn-error">
|
||||||
|
<KeyCombo keys={['Esc']} />
|
||||||
|
</button>
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -39,8 +39,8 @@
|
|||||||
Launch Terminal
|
Launch Terminal
|
||||||
</button>
|
</button>
|
||||||
<label class="label cursor-pointer flex items-center space-x-2">
|
<label class="label cursor-pointer flex items-center space-x-2">
|
||||||
|
<span class="label-text">Launch with long-lived credentials</span>
|
||||||
<input type="checkbox" class="checkbox checkbox-sm" bind:checked={launchBase}>
|
<input type="checkbox" class="checkbox checkbox-sm" bind:checked={launchBase}>
|
||||||
<span class="label-text">Launch with base credentials</span>
|
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
{:else if status === 'empty'}
|
{:else if status === 'empty'}
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { draw, fade } from 'svelte/transition';
|
import { draw, fade } from 'svelte/transition';
|
||||||
|
|
||||||
import { appState, completeRequest } from '../lib/state.js';
|
import { appState, cleanupRequest } from '../lib/state.js';
|
||||||
|
|
||||||
let success = false;
|
let success = false;
|
||||||
let error = null;
|
let error = null;
|
||||||
@ -13,7 +13,7 @@
|
|||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
window.setTimeout(
|
window.setTimeout(
|
||||||
completeRequest,
|
cleanupRequest,
|
||||||
// Extra 50ms so the window can finish disappearing before the redraw
|
// Extra 50ms so the window can finish disappearing before the redraw
|
||||||
Math.min(5000, $appState.config.rehide_ms + 50),
|
Math.min(5000, $appState.config.rehide_ms + 50),
|
||||||
)
|
)
|
||||||
@ -22,7 +22,7 @@
|
|||||||
|
|
||||||
|
|
||||||
<div class="flex flex-col h-screen items-center justify-center max-w-max m-auto">
|
<div class="flex flex-col h-screen items-center justify-center max-w-max m-auto">
|
||||||
{#if $appState.currentRequest.approval === 'Approved'}
|
{#if $appState.currentRequest.response.approval === 'Approved'}
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-36 h-36" fill="none" viewBox="0 0 24 24" stroke-width="1" stroke="currentColor">
|
<svg xmlns="http://www.w3.org/2000/svg" class="w-36 h-36" fill="none" viewBox="0 0 24 24" stroke-width="1" stroke="currentColor">
|
||||||
<path in:draw="{{duration: drawDuration}}" stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
<path in:draw="{{duration: drawDuration}}" stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
</svg>
|
</svg>
|
||||||
@ -33,6 +33,6 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div in:fade="{{duration: fadeDuration, delay: fadeDelay}}" class="text-2xl font-bold">
|
<div in:fade="{{duration: fadeDuration, delay: fadeDelay}}" class="text-2xl font-bold">
|
||||||
{$appState.currentRequest.approval}!
|
{$appState.currentRequest.response.approval}!
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
Reference in New Issue
Block a user