Compare commits

..

No commits in common. "docker-helper" and "master" have entirely different histories.

18 changed files with 112 additions and 533 deletions

View File

@ -22,8 +22,6 @@ use crate::proto::{
ShortcutAction, ShortcutAction,
}; };
mod docker;
#[derive(Debug, Parser)] #[derive(Debug, Parser)]
#[command( #[command(
@ -72,9 +70,6 @@ pub enum Action {
Exec(ExecArgs), Exec(ExecArgs),
/// Invoke an action normally triggered by hotkey (e.g. launch terminal) /// Invoke an action normally triggered by hotkey (e.g. launch terminal)
Shortcut(InvokeArgs), Shortcut(InvokeArgs),
/// Interact with Docker credentials via the docker-credential-helper protocol
#[command(subcommand)]
Docker(DockerCmd),
} }
@ -106,19 +101,8 @@ pub struct InvokeArgs {
} }
#[derive(Debug, Subcommand)]
pub enum DockerCmd {
/// Get a stored Docker credential
Get,
/// Store a new Docker credential
Store,
/// Remove a stored Docker credential
Erase,
}
pub fn get(args: GetArgs, global: GlobalArgs) -> anyhow::Result<()> { pub fn get(args: GetArgs, global: GlobalArgs) -> anyhow::Result<()> {
let req = CliRequest::GetAwsCredential { let req = CliRequest::GetCredential {
name: args.name, name: args.name,
base: args.base, base: args.base,
}; };
@ -145,7 +129,7 @@ pub fn exec(args: ExecArgs, global: GlobalArgs) -> anyhow::Result<()> {
let mut cmd = ChildCommand::new(cmd_name); let mut cmd = ChildCommand::new(cmd_name);
cmd.args(cmd_line); cmd.args(cmd_line);
let req = CliRequest::GetAwsCredential { let req = CliRequest::GetCredential {
name: args.get_args.name, name: args.get_args.name,
base: args.get_args.base, base: args.get_args.base,
}; };
@ -201,20 +185,10 @@ pub fn invoke_shortcut(args: InvokeArgs, global: GlobalArgs) -> anyhow::Result<(
} }
pub fn docker_credential_helper(cmd: DockerCmd, global_args: GlobalArgs) -> anyhow::Result<()> {
match cmd {
DockerCmd::Get => docker::docker_get(global_args),
DockerCmd::Store => docker::docker_store(global_args),
DockerCmd::Erase => todo!(),
}
}
// Explanation for double-result: the server will return a (serialized) Result // Explanation for double-result: the server will return a (serialized) Result
// to indicate when the operation succeeded or failed, which we deserialize. // to indicate when the operation succeeded or failed, which we deserialize.
// However, the operation may fail to even communicate with the server, in // However, the operation may fail to even communicate with the server, in
// which case we return the outer Result // which case we return the outer Result
// (probably this should be modeled differently)
#[tokio::main] #[tokio::main]
async fn make_request( async fn make_request(
addr: Option<PathBuf>, addr: Option<PathBuf>,

View File

@ -1,43 +0,0 @@
use std::io::{self, Read};
use anyhow::bail;
use crate::proto::{CliResponse, DockerCredential};
use super::{
CliCredential,
CliRequest,
GlobalArgs
};
pub fn docker_store(global_args: GlobalArgs) -> anyhow::Result<()> {
let input: DockerCredential = serde_json::from_reader(io::stdin())?;
let req = CliRequest::SaveCredential {
name: input.username.clone(),
is_default: false, // is_default doesn't really mean anything for Docker credentials
credential: CliCredential::Docker(input),
};
match super::make_request(global_args.server_addr, &req)?? {
CliResponse::Empty => Ok(()),
r => bail!("Unexpected response from server: {r}"),
}
}
pub fn docker_get(global_args: GlobalArgs) -> anyhow::Result<()> {
let mut server_url = String::new();
io::stdin().read_to_string(&mut server_url)?;
let req = CliRequest::GetDockerCredential {
server_url: server_url.trim().to_owned()
};
match super::make_request(global_args.server_addr, &req)?? {
CliResponse::Credential(CliCredential::Docker(d)) => {
println!("{}", serde_json::to_string(&d)?);
},
r => bail!("Unexpected response from server: {r}"),
}
Ok(())
}

View File

@ -5,7 +5,6 @@ pub use cli::{
exec, exec,
get, get,
invoke_shortcut, invoke_shortcut,
docker_credential_helper,
}; };
pub(crate) use platform::connect; pub(crate) use platform::connect;

View File

@ -11,7 +11,6 @@ fn main() {
Some(Action::Get(args)) => creddy_cli::get(args, cli.global_args), Some(Action::Get(args)) => creddy_cli::get(args, cli.global_args),
Some(Action::Exec(args)) => creddy_cli::exec(args, cli.global_args), Some(Action::Exec(args)) => creddy_cli::exec(args, cli.global_args),
Some(Action::Shortcut(args)) => creddy_cli::invoke_shortcut(args, cli.global_args), Some(Action::Shortcut(args)) => creddy_cli::invoke_shortcut(args, cli.global_args),
Some(Action::Docker(cmd)) => creddy_cli::docker_credential_helper(cmd, cli.global_args),
}; };
if let Err(e) = res { if let Err(e) = res {

View File

@ -10,18 +10,10 @@ use serde::{Serialize, Deserialize};
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub enum CliRequest { pub enum CliRequest {
GetAwsCredential { GetCredential {
name: Option<String>, name: Option<String>,
base: bool, base: bool,
}, },
GetDockerCredential {
server_url: String,
},
SaveCredential {
name: String,
is_default: bool,
credential: CliCredential,
},
InvokeShortcut(ShortcutAction), InvokeShortcut(ShortcutAction),
} }
@ -44,7 +36,6 @@ impl Display for CliResponse {
match self { match self {
CliResponse::Credential(CliCredential::AwsBase(_)) => write!(f, "Credential (AwsBase)"), CliResponse::Credential(CliCredential::AwsBase(_)) => write!(f, "Credential (AwsBase)"),
CliResponse::Credential(CliCredential::AwsSession(_)) => write!(f, "Credential (AwsSession)"), CliResponse::Credential(CliCredential::AwsSession(_)) => write!(f, "Credential (AwsSession)"),
CliResponse::Credential(CliCredential::Docker(_)) => write!(f, "Credential (Docker)"),
CliResponse::Empty => write!(f, "Empty"), CliResponse::Empty => write!(f, "Empty"),
} }
} }
@ -55,7 +46,6 @@ impl Display for CliResponse {
pub enum CliCredential { pub enum CliCredential {
AwsBase(AwsBaseCredential), AwsBase(AwsBaseCredential),
AwsSession(AwsSessionCredential), AwsSession(AwsSessionCredential),
Docker(DockerCredential),
} }
@ -85,16 +75,6 @@ pub struct AwsSessionCredential {
fn default_aws_version() -> usize { 1 } fn default_aws_version() -> usize { 1 }
#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct DockerCredential {
#[serde(rename = "ServerURL")]
pub server_url: String,
pub username: String,
pub secret: String,
}
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub struct ServerError { pub struct ServerError {
code: String, code: String,

View File

@ -1,11 +0,0 @@
CREATE TABLE docker_credentials (
id BLOB UNIQUE NOT NULL,
-- The Docker credential helper protocol only sends the server_url, so
-- we should guarantee that we will only ever have one matching credential.
-- Also, it's easier to go from unique -> not-unique than vice versa if we
-- decide that's necessary in the future
server_url TEXT UNIQUE NOT NULL,
username TEXT NOT NULL,
secret_enc BLOB NOT NULL,
nonce BLOB NOT NULL
);

View File

@ -1,196 +0,0 @@
use chacha20poly1305::XNonce;
use serde::{Serialize, Deserialize};
use sqlx::{
FromRow,
Sqlite,
Transaction,
types::Uuid,
};
use super::{Credential, Crypto, PersistentCredential};
use crate::errors::*;
#[derive(Debug, Clone, FromRow)]
pub struct DockerRow {
id: Uuid,
server_url: String,
username: String,
secret_enc: Vec<u8>,
nonce: Vec<u8>,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct DockerCredential {
#[serde(rename = "ServerURL")]
pub server_url: String,
pub username: String,
pub secret: String,
}
impl PersistentCredential for DockerCredential {
type Row = DockerRow;
fn type_name() -> &'static str { "docker" }
fn into_credential(self) -> Credential { Credential::Docker(self) }
fn row_id(row: &DockerRow) -> Uuid { row.id }
fn from_row(row: DockerRow, crypto: &Crypto) -> Result<Self, LoadCredentialsError> {
let nonce = XNonce::clone_from_slice(&row.nonce);
let secret_bytes = crypto.decrypt(&nonce, &row.secret_enc)?;
let secret = String::from_utf8(secret_bytes)
.map_err(|_| LoadCredentialsError::InvalidData)?;
Ok(DockerCredential {
server_url: row.server_url,
username: row.username,
secret
})
}
async fn save_details(&self, id: &Uuid, crypto: &Crypto, txn: &mut Transaction<'_, Sqlite>) -> Result<(), SaveCredentialsError> {
let (nonce, ciphertext) = crypto.encrypt(self.secret.as_bytes())?;
let nonce_bytes = &nonce.as_slice();
sqlx::query!(
"INSERT OR REPLACE INTO docker_credentials (
id,
server_url,
username,
secret_enc,
nonce
)
VALUES (?, ?, ?, ?, ?)",
id, self.server_url, self.username, ciphertext, nonce_bytes,
).execute(&mut **txn).await?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::credentials::CredentialRecord;
use creddy_cli::proto::DockerCredential as CliDockerCredential;
use sqlx::SqlitePool;
use sqlx::types::uuid::uuid;
fn test_credential() -> DockerCredential {
DockerCredential {
server_url: "https://registry.jfmonty2.com".into(),
username: "joe@jfmonty2.com".into(),
secret: "correct horse battery staple".into(),
}
}
fn test_credential_2() -> DockerCredential {
DockerCredential {
server_url: "https://index.docker.io/v1".into(),
username: "test@example.com".into(),
secret: "a very secure passphrase".into(),
}
}
fn test_record() -> CredentialRecord {
CredentialRecord {
id: uuid!("00000000-0000-0000-0000-000000000000"),
name: "docker_test".into(),
is_default: false,
credential: Credential::Docker(test_credential()),
}
}
fn test_record_2() -> CredentialRecord {
CredentialRecord {
id: uuid!("ffffffff-ffff-ffff-ffff-ffffffffffff"),
name: "docker_test_2".into(),
is_default: false,
credential: Credential::Docker(test_credential_2()),
}
}
#[sqlx::test]
fn test_save(pool: SqlitePool) {
let crypt = Crypto::random();
test_record().save(&crypt, &pool).await
.expect("Failed to save record");
}
#[sqlx::test(fixtures("docker_credentials"))]
fn test_load(pool: SqlitePool) {
let crypt = Crypto::fixed();
let id = uuid!("00000000-0000-0000-0000-000000000000");
let loaded = DockerCredential::load(&id, &crypt, &pool).await
.expect("Failed to load record");
assert_eq!(test_credential(), loaded);
}
#[sqlx::test(fixtures("docker_credentials"))]
async fn test_overwrite(pool: SqlitePool) {
let crypt = Crypto::fixed();
let mut record = test_record_2();
// give it the same id as test_record so that it overwrites
let id = uuid!("00000000-0000-0000-0000-000000000000");
record.id = id;
record.save(&crypt, &pool).await
.expect("Failed to overwrite original record with second record");
let loaded = DockerCredential::load(&id, &crypt, &pool).await
.expect("Failed to load again after overwriting");
assert_eq!(test_credential_2(), loaded);
}
#[sqlx::test(fixtures("docker_credentials"))]
async fn test_list(pool: SqlitePool) {
let crypt = Crypto::fixed();
let records = CredentialRecord::list(&crypt, &pool).await
.expect("Failed to list credentials");
assert_eq!(test_record(), records[0]);
}
// make sure that CLI credentials and app credentials don't drift apart
#[test]
fn test_cli_to_app() {
let cli_creds = CliDockerCredential {
server_url: "https://registry.jfmonty2.com".into(),
username: "joe@jfmonty2.com".into(),
secret: "correct horse battery staple".into(),
};
let json = serde_json::to_string(&cli_creds).unwrap();
let computed: DockerCredential = serde_json::from_str(&json)
.expect("Failed to deserialize Docker credentials from CLI -> main app");
assert_eq!(test_credential(), computed);
}
#[test]
fn test_app_to_cli() {
let app_creds = test_credential();
let json = serde_json::to_string(&app_creds).unwrap();
let computed: CliDockerCredential = serde_json::from_str(&json)
.expect("Failed to deserialize Docker credentials from main app -> CLI");
let expected = CliDockerCredential {
server_url: "https://registry.jfmonty2.com".into(),
username: "joe@jfmonty2.com".into(),
secret: "correct horse battery staple".into(),
};
assert_eq!(expected, computed);
}
}

View File

@ -1,11 +0,0 @@
INSERT INTO credentials (id, name, credential_type, is_default, created_at)
VALUES (X'00000000000000000000000000000000', 'docker_test', 'docker', 0, 1726756380);
INSERT INTO docker_credentials (id, server_url, username, secret_enc, nonce)
VALUES (
X'00000000000000000000000000000000',
'https://registry.jfmonty2.com',
'joe@jfmonty2.com',
X'C0B36EE54539D4113A8F73E99FB96B2BF4D87E91F7C3B48256C07E83E3E7EC738888B2FDE2B4DB0BE48BEFDE',
X'C5F7F627BBE09A1BB275BE8D2390596C76143881A7766E60'
);

View File

@ -17,9 +17,6 @@ pub use aws::{AwsBaseCredential, AwsSessionCredential};
mod crypto; mod crypto;
pub use crypto::Crypto; pub use crypto::Crypto;
mod docker;
pub use docker::DockerCredential;
mod record; mod record;
pub use record::CredentialRecord; pub use record::CredentialRecord;
@ -35,7 +32,6 @@ pub use ssh::SshKey;
pub enum Credential { pub enum Credential {
AwsBase(AwsBaseCredential), AwsBase(AwsBaseCredential),
AwsSession(AwsSessionCredential), AwsSession(AwsSessionCredential),
Docker(DockerCredential),
Ssh(SshKey), Ssh(SshKey),
} }
@ -83,23 +79,6 @@ pub trait PersistentCredential: for<'a> Deserialize<'a> + Sized {
Self::from_row(row, crypto) Self::from_row(row, crypto)
} }
async fn load_by<T>(column: &str, value: T, crypto: &Crypto, pool: &SqlitePool) -> Result<Self, LoadCredentialsError>
where T: Send + for<'q> sqlx::Encode<'q, Sqlite> + sqlx::Type<Sqlite>
{
let query = format!(
"SELECT * FROM {} where {} = ?",
Self::table_name(),
column,
);
let row: Self::Row = sqlx::query_as(&query)
.bind(value)
.fetch_optional(pool)
.await?
.ok_or(LoadCredentialsError::NoCredentials)?;
Self::from_row(row, crypto)
}
async fn load_default(crypto: &Crypto, pool: &SqlitePool) -> Result<Self, LoadCredentialsError> { async fn load_default(crypto: &Crypto, pool: &SqlitePool) -> Result<Self, LoadCredentialsError> {
let q = format!( let q = format!(
"SELECT details.* "SELECT details.*
@ -120,15 +99,15 @@ pub trait PersistentCredential: for<'a> Deserialize<'a> + Sized {
async fn list(crypto: &Crypto, pool: &SqlitePool) -> Result<Vec<(Uuid, Credential)>, LoadCredentialsError> { async fn list(crypto: &Crypto, pool: &SqlitePool) -> Result<Vec<(Uuid, Credential)>, LoadCredentialsError> {
let q = format!( let q = format!(
"SELECT details.* "SELECT details.*
FROM FROM
{} details {} details
JOIN credentials c JOIN credentials c
ON c.id = details.id ON c.id = details.id
ORDER BY c.created_at", ORDER BY c.created_at",
Self::table_name(), Self::table_name(),
); );
let mut rows = sqlx::query_as::<_, Self::Row>(&q).fetch(pool); let mut rows = sqlx::query_as::<_, Self::Row>(&q).fetch(pool);
let mut creds = Vec::new(); let mut creds = Vec::new();
while let Some(row) = rows.try_next().await? { while let Some(row) = rows.try_next().await? {
let id = Self::row_id(&row); let id = Self::row_id(&row);

View File

@ -20,7 +20,6 @@ use super::{
AwsBaseCredential, AwsBaseCredential,
Credential, Credential,
Crypto, Crypto,
DockerCredential,
PersistentCredential, PersistentCredential,
SshKey, SshKey,
}; };
@ -52,7 +51,6 @@ impl CredentialRecord {
let type_name = match &self.credential { let type_name = match &self.credential {
Credential::AwsBase(_) => AwsBaseCredential::type_name(), Credential::AwsBase(_) => AwsBaseCredential::type_name(),
Credential::Ssh(_) => SshKey::type_name(), Credential::Ssh(_) => SshKey::type_name(),
Credential::Docker(_) => DockerCredential::type_name(),
_ => return Err(SaveCredentialsError::NotPersistent), _ => return Err(SaveCredentialsError::NotPersistent),
}; };
@ -88,7 +86,6 @@ impl CredentialRecord {
match &self.credential { match &self.credential {
Credential::AwsBase(b) => b.save_details(&self.id, crypto, &mut txn).await, Credential::AwsBase(b) => b.save_details(&self.id, crypto, &mut txn).await,
Credential::Ssh(s) => s.save_details(&self.id, crypto, &mut txn).await, Credential::Ssh(s) => s.save_details(&self.id, crypto, &mut txn).await,
Credential::Docker(d) => d.save_details(&self.id, crypto, &mut txn).await,
_ => Err(SaveCredentialsError::NotPersistent), _ => Err(SaveCredentialsError::NotPersistent),
}?; }?;
@ -170,11 +167,6 @@ impl CredentialRecord {
.ok_or(LoadCredentialsError::InvalidData)?; .ok_or(LoadCredentialsError::InvalidData)?;
records.push(Self::from_parts(parent, credential)); records.push(Self::from_parts(parent, credential));
} }
for (id, credential) in DockerCredential::list(crypto, pool).await? {
let parent = parent_map.remove(&id)
.ok_or(LoadCredentialsError::InvalidData)?;
records.push(Self::from_parts(parent, credential));
}
Ok(records) Ok(records)
} }

View File

@ -173,7 +173,7 @@ pub enum HandlerError {
StreamIOError(#[from] std::io::Error), StreamIOError(#[from] std::io::Error),
#[error("Received invalid UTF-8 in request")] #[error("Received invalid UTF-8 in request")]
InvalidUtf8(#[from] FromUtf8Error), InvalidUtf8(#[from] FromUtf8Error),
#[error("Request malformed: {0}")] #[error("HTTP request malformed")]
BadRequest(#[from] serde_json::Error), BadRequest(#[from] serde_json::Error),
#[error("HTTP request too large")] #[error("HTTP request too large")]
RequestTooLarge, RequestTooLarge,
@ -183,8 +183,6 @@ pub enum HandlerError {
Internal(#[from] RecvError), Internal(#[from] RecvError),
#[error("Error accessing credentials: {0}")] #[error("Error accessing credentials: {0}")]
NoCredentials(#[from] GetCredentialsError), NoCredentials(#[from] GetCredentialsError),
#[error("Error saving credentials: {0}")]
SaveCredentials(#[from] SaveCredentialsError),
#[error("Error getting client details: {0}")] #[error("Error getting client details: {0}")]
ClientInfo(#[from] ClientInfoError), ClientInfo(#[from] ClientInfoError),
#[error("Error from Tauri: {0}")] #[error("Error from Tauri: {0}")]

View File

@ -16,6 +16,7 @@ use crate::terminal;
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
pub struct AwsRequestNotification { pub struct AwsRequestNotification {
pub id: u64,
pub client: Client, pub client: Client,
pub name: Option<String>, pub name: Option<String>,
pub base: bool, pub base: bool,
@ -24,46 +25,27 @@ pub struct AwsRequestNotification {
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
pub struct SshRequestNotification { pub struct SshRequestNotification {
pub id: u64,
pub client: Client, pub client: Client,
pub key_name: String, pub key_name: String,
} }
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct DockerRequestNotification {
pub client: Client,
pub server_url: String,
}
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(tag = "type")] #[serde(tag = "type")]
pub enum RequestNotificationDetail { pub enum RequestNotification {
Aws(AwsRequestNotification), Aws(AwsRequestNotification),
Ssh(SshRequestNotification), Ssh(SshRequestNotification),
Docker(DockerRequestNotification),
} }
impl RequestNotificationDetail { impl RequestNotification {
pub fn new_aws(client: Client, name: Option<String>, base: bool) -> Self { pub fn new_aws(id: u64, client: Client, name: Option<String>, base: bool) -> Self {
Self::Aws(AwsRequestNotification {client, name, base}) Self::Aws(AwsRequestNotification {id, client, name, base})
} }
pub fn new_ssh(client: Client, key_name: String) -> Self { pub fn new_ssh(id: u64, client: Client, key_name: String) -> Self {
Self::Ssh(SshRequestNotification {client, key_name}) Self::Ssh(SshRequestNotification {id, client, key_name})
} }
pub fn new_docker(client: Client, server_url: String) -> Self {
Self::Docker(DockerRequestNotification {client, server_url})
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct RequestNotification {
pub id: u64,
#[serde(flatten)]
pub detail: RequestNotificationDetail,
} }

View File

@ -21,7 +21,6 @@ fn main() {
Some(Action::Get(args)) => creddy_cli::get(args, cli.global_args), Some(Action::Get(args)) => creddy_cli::get(args, cli.global_args),
Some(Action::Exec(args)) => creddy_cli::exec(args, cli.global_args), Some(Action::Exec(args)) => creddy_cli::exec(args, cli.global_args),
Some(Action::Shortcut(args)) => creddy_cli::invoke_shortcut(args, cli.global_args), Some(Action::Shortcut(args)) => creddy_cli::invoke_shortcut(args, cli.global_args),
Some(Action::Docker(cmd)) => creddy_cli::docker_credential_helper(cmd, cli.global_args),
}; };
if let Err(e) = res { if let Err(e) = res {

View File

@ -11,7 +11,7 @@ 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, RequestNotification};
use crate::state::AppState; use crate::state::AppState;
use super::{CloseWaiter, Stream}; use super::{CloseWaiter, Stream};
@ -40,7 +40,7 @@ async fn handle(
// corrupt the framing. Clients don't seem to behave that way though? // corrupt the framing. Clients don't seem to behave that way though?
let waiter = CloseWaiter { stream: adapter.get_mut() }; let waiter = CloseWaiter { stream: adapter.get_mut() };
let resp = sign_request(req, app_handle.clone(), client_pid, waiter).await?; let resp = sign_request(req, app_handle.clone(), client_pid, waiter).await?;
// have to do this before we send since we can't inspect the message after // have to do this before we send since we can't inspect the message after
let is_failure = matches!(resp, Message::Failure); let is_failure = matches!(resp, Message::Failure);
adapter.send(resp).await?; adapter.send(resp).await?;
@ -69,21 +69,47 @@ async fn sign_request(
req: SignRequest, req: SignRequest,
app_handle: AppHandle, app_handle: AppHandle,
client_pid: u32, client_pid: u32,
waiter: CloseWaiter<'_>, mut waiter: CloseWaiter<'_>,
) -> Result<Message, HandlerError> { ) -> Result<Message, HandlerError> {
let state = app_handle.state::<AppState>(); let state = app_handle.state::<AppState>();
let rehide_ms = {
let config = state.config.read().await;
config.rehide_ms
};
let client = clientinfo::get_client(client_pid, false)?; let client = clientinfo::get_client(client_pid, false)?;
let key_name = state.ssh_name_from_pubkey(&req.pubkey_blob).await?; let lease = state.acquire_visibility_lease(rehide_ms).await
let detail = RequestNotificationDetail::new_ssh(client, key_name.clone()); .map_err(|_e| HandlerError::NoMainWindow)?;
let response = super::send_credentials_request(detail, app_handle.clone(), waiter).await?; let (chan_send, chan_recv) = oneshot::channel();
match response.approval { let request_id = state.register_request(chan_send).await;
Approval::Approved => {
let key = state.sshkey_by_name(&key_name).await?; let proceed = async {
let sig = key.sign_request(&req)?; let key_name = state.ssh_name_from_pubkey(&req.pubkey_blob).await?;
Ok(Message::SignResponse(sig)) let notification = RequestNotification::new_ssh(request_id, client, key_name.clone());
}, app_handle.emit("credential-request", &notification)?;
Approval::Denied => Err(HandlerError::Abandoned),
let response = tokio::select! {
r = chan_recv => r?,
_ = waiter.wait_for_close() => {
app_handle.emit("request-cancelled", request_id)?;
return Err(HandlerError::Abandoned);
},
};
if let Approval::Denied = response.approval {
return Ok(Message::Failure);
}
let key = state.sshkey_by_name(&key_name).await?;
let sig = key.sign_request(&req)?;
Ok(Message::SignResponse(sig))
};
let res = proceed.await;
if let Err(_) = &res {
state.unregister_request(request_id).await;
} }
lease.release();
res
} }

View File

@ -1,16 +1,10 @@
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 tokio::sync::oneshot;
use crate::clientinfo::{self, Client}; use crate::clientinfo::{self, Client};
use crate::credentials::{
Credential,
CredentialRecord,
Crypto
};
use crate::errors::*; use crate::errors::*;
use crate::ipc::{Approval, AwsRequestNotification, RequestNotificationDetail, RequestResponse}; use crate::ipc::{Approval, RequestNotification};
use crate::shortcuts::{self, ShortcutAction}; use crate::shortcuts::{self, ShortcutAction};
use crate::state::AppState; use crate::state::AppState;
use super::{ use super::{
@ -52,15 +46,9 @@ async fn handle(
let req: CliRequest = serde_json::from_slice(&buf)?; let req: CliRequest = serde_json::from_slice(&buf)?;
let res = match req { let res = match req {
CliRequest::GetAwsCredential{ name, base } => get_aws_credentials( CliRequest::GetCredential{ name, base } => get_aws_credentials(
name, base, client, app_handle, waiter name, base, client, app_handle, waiter
).await, ).await,
CliRequest::GetDockerCredential{ server_url } => get_docker_credentials (
server_url, client, app_handle, waiter
).await,
CliRequest::SaveCredential{ name, is_default, credential } => save_credential(
name, is_default, credential, app_handle
).await,
CliRequest::InvokeShortcut(action) => invoke_shortcut(action).await, CliRequest::InvokeShortcut(action) => invoke_shortcut(action).await,
}; };
@ -86,64 +74,59 @@ async fn get_aws_credentials(
base: bool, base: bool,
client: Client, client: Client,
app_handle: AppHandle, app_handle: AppHandle,
waiter: CloseWaiter<'_>, mut waiter: CloseWaiter<'_>,
) -> Result<CliResponse, HandlerError> {
let detail = RequestNotificationDetail::new_aws(client, name.clone(), base);
let response = super::send_credentials_request(detail, app_handle.clone(), waiter).await?;
match response.approval {
Approval::Approved => {
let state = app_handle.state::<AppState>();
if response.base {
let creds = state.get_aws_base(name).await?;
Ok(CliResponse::Credential(CliCredential::AwsBase(creds)))
}
else {
let creds = state.get_aws_session(name).await?.clone();
Ok(CliResponse::Credential(CliCredential::AwsSession(creds)))
}
},
Approval::Denied => Err(HandlerError::Denied),
}
}
async fn get_docker_credentials(
server_url: String,
client: Client,
app_handle: AppHandle,
waiter: CloseWaiter<'_>,
) -> Result<CliResponse, HandlerError> {
let detail = RequestNotificationDetail::new_docker(client, server_url.clone());
let response = super::send_credentials_request(detail, app_handle.clone(), waiter).await?;
match response.approval {
Approval::Approved => {
let state = app_handle.state::<AppState>();
let creds = state.get_docker_credential(&server_url).await?;
Ok(CliResponse::Credential(CliCredential::Docker(creds)))
},
Approval::Denied => {
Err(HandlerError::Denied)
},
}
}
pub async fn save_credential(
name: String,
is_default: bool,
credential: Credential,
app_handle: AppHandle,
) -> Result<CliResponse, HandlerError> { ) -> Result<CliResponse, HandlerError> {
let state = app_handle.state::<AppState>(); let state = app_handle.state::<AppState>();
let rehide_ms = {
// eventually ask the frontend to unlock here let config = state.config.read().await;
config.rehide_ms
// a bit weird but convenient
let random_bytes = Crypto::salt();
let id = Uuid::from_slice(&random_bytes[..16]).unwrap();
let record = CredentialRecord {
id, name, is_default, credential
}; };
state.save_credential(record).await?; let lease = state.acquire_visibility_lease(rehide_ms).await
.map_err(|_e| HandlerError::NoMainWindow)?; // automate this conversion eventually?
Ok(CliResponse::Empty) let (chan_send, chan_recv) = oneshot::channel();
let request_id = state.register_request(chan_send).await;
// if an error occurs in any of the following, we want to abort the operation
// but ? returns immediately, and we want to unregister the request before returning
// so we bundle it all up in an async block and return a Result so we can handle errors
let proceed = async {
let notification = RequestNotification::new_aws(
request_id, client, name.clone(), base
);
app_handle.emit("credential-request", &notification)?;
let response = tokio::select! {
r = chan_recv => r?,
_ = waiter.wait_for_close() => {
app_handle.emit("request-cancelled", request_id)?;
return Err(HandlerError::Abandoned);
},
};
match response.approval {
Approval::Approved => {
if response.base {
let creds = state.get_aws_base(name).await?;
Ok(CliResponse::Credential(CliCredential::AwsBase(creds)))
}
else {
let creds = state.get_aws_session(name).await?.clone();
Ok(CliResponse::Credential(CliCredential::AwsSession(creds)))
}
},
Approval::Denied => Err(HandlerError::Denied),
}
};
let result = match proceed.await {
Ok(r) => Ok(r),
Err(e) => {
state.unregister_request(request_id).await;
Err(e)
},
};
lease.release();
result
} }

View File

@ -3,23 +3,13 @@ use std::future::Future;
use tauri::{ use tauri::{
AppHandle, AppHandle,
async_runtime as rt, async_runtime as rt,
Manager,
}; };
use tokio::io::AsyncReadExt; use tokio::io::AsyncReadExt;
use tokio::sync::oneshot;
use serde::{Serialize, Deserialize}; use serde::{Serialize, Deserialize};
use crate::clientinfo::Client; use crate::credentials::{AwsBaseCredential, AwsSessionCredential};
use crate::credentials::{
AwsBaseCredential,
AwsSessionCredential,
Credential,
DockerCredential,
};
use crate::errors::*; use crate::errors::*;
use crate::ipc::{RequestNotification, RequestNotificationDetail, RequestResponse};
use crate::shortcuts::ShortcutAction; use crate::shortcuts::ShortcutAction;
use crate::state::AppState;
pub mod creddy_server; pub mod creddy_server;
pub mod agent; pub mod agent;
@ -31,18 +21,10 @@ use platform::Stream;
// that would make it impossible to build a completely static-linked version // that would make it impossible to build a completely static-linked version
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub enum CliRequest { pub enum CliRequest {
GetAwsCredential { GetCredential {
name: Option<String>, name: Option<String>,
base: bool, base: bool,
}, },
GetDockerCredential {
server_url: String,
},
SaveCredential {
name: String,
is_default: bool,
credential: Credential,
},
InvokeShortcut(ShortcutAction), InvokeShortcut(ShortcutAction),
} }
@ -58,7 +40,6 @@ pub enum CliResponse {
pub enum CliCredential { pub enum CliCredential {
AwsBase(AwsBaseCredential), AwsBase(AwsBaseCredential),
AwsSession(AwsSessionCredential), AwsSession(AwsSessionCredential),
Docker(DockerCredential),
} }
@ -106,48 +87,6 @@ fn serve<H, F>(sock_name: &str, app_handle: AppHandle, handler: H) -> std::io::R
} }
async fn send_credentials_request(
detail: RequestNotificationDetail,
app_handle: AppHandle,
mut waiter: CloseWaiter<'_>
) -> Result<RequestResponse, HandlerError> {
let state = app_handle.state::<AppState>();
let rehide_ms = {
let config = state.config.read().await;
config.rehide_ms
};
let lease = state.acquire_visibility_lease(rehide_ms).await
.map_err(|_e| HandlerError::NoMainWindow)?;
let (chan_send, chan_recv) = oneshot::channel();
let request_id = state.register_request(chan_send).await;
let notification = RequestNotification { id: request_id, detail };
// the following could fail in various ways, but we want to make sure
// the request gets unregistered on any failure, so we wrap this all
// up in an async block so that we only have to handle the error case once
let proceed = async {
app_handle.emit("credential-request", &notification)?;
tokio::select! {
r = chan_recv => Ok(r?),
_ = waiter.wait_for_close() => {
app_handle.emit("request-cancelled", request_id)?;
Err(HandlerError::Abandoned)
},
}
};
let res = proceed.await;
if let Err(_) = &res {
state.unregister_request(request_id).await;
}
lease.release();
res
}
#[cfg(unix)] #[cfg(unix)]
mod platform { mod platform {
use std::io::ErrorKind; use std::io::ErrorKind;

View File

@ -19,7 +19,6 @@ use crate::app;
use crate::credentials::{ use crate::credentials::{
AppSession, AppSession,
AwsSessionCredential, AwsSessionCredential,
DockerCredential,
SshKey, SshKey,
}; };
use crate::{config, config::AppConfig}; use crate::{config, config::AppConfig};
@ -194,7 +193,7 @@ impl AppState {
pub async fn update_config(&self, new_config: AppConfig) -> Result<(), SetupError> { pub async fn update_config(&self, new_config: AppConfig) -> Result<(), SetupError> {
let mut live_config = self.config.write().await; let mut live_config = self.config.write().await;
// update autostart if necessary // update autostart if necessary
if new_config.start_on_login != live_config.start_on_login { if new_config.start_on_login != live_config.start_on_login {
config::set_auto_launch(new_config.start_on_login)?; config::set_auto_launch(new_config.start_on_login)?;
@ -323,13 +322,6 @@ impl AppState {
Ok(k) Ok(k)
} }
pub async fn get_docker_credential(&self, server_url: &str) -> Result<DockerCredential, GetCredentialsError> {
let app_session = self.app_session.read().await;
let crypto = app_session.try_get_crypto()?;
let d = DockerCredential::load_by("server_url", server_url.to_owned(), crypto, &self.pool).await?;
Ok(d)
}
pub async fn signal_activity(&self) { pub async fn signal_activity(&self) {
let mut last_activity = self.last_activity.write().await; let mut last_activity = self.last_activity.write().await;
*last_activity = OffsetDateTime::now_utc(); *last_activity = OffsetDateTime::now_utc();

View File

@ -34,7 +34,7 @@
<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 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>
@ -51,8 +51,6 @@
{/if} {/if}
{: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'}
{appName ? `"${appName}"` : 'An application'} would like to use your Docker credentials for <code>{$appState.currentRequest.server_url}</code>.
{/if} {/if}
</h2> </h2>