serializable structured errors

This commit is contained in:
Joseph Montanaro 2022-12-29 16:40:48 -08:00
parent cb26201506
commit e0c4c849dc
4 changed files with 174 additions and 54 deletions

View File

@ -1,3 +1,7 @@
use std::error::Error;
use std::convert::AsRef;
use strum_macros::AsRefStr;
use thiserror::Error as ThisError;
use aws_sdk_sts::{
@ -10,35 +14,70 @@ use sqlx::{
};
use serde::{Serialize, Serializer, ser::SerializeMap};
use strum_macros::IntoStaticStr;
pub struct SerializeError<E> {
pub err: E
}
// pub struct SerializeError<E> {
// pub err: E,
// }
impl<E: std::error::Error> Serialize for SerializeError<E>
// impl<E: std::error::Error> Serialize for SerializeError<E>
// {
// fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
// let mut map = serializer.serialize_map(None)?;
// map.serialize_entry("msg", &format!("{}", self.err))?;
// if let Some(src) = self.err.source() {
// let ser_src = SerializeError { err: src };
// map.serialize_entry("source", &ser_src)?;
// }
// map.end()
// }
// }
// impl<E: std::error::Error> From<E> for SerializeError<E> {
// fn from(err: E) -> Self {
// SerializeError { err }
// }
// }
fn serialize_basic_err<E, S>(err: &E, serializer: S) -> Result<S::Ok, S::Error>
where
E: std::error::Error + AsRef<str>,
S: Serializer,
{
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
let mut map = serializer.serialize_map(None)?;
map.serialize_entry("msg", &format!("{}", self.err))?;
if let Some(src) = self.err.source() {
let ser_src = SerializeError { err: src };
map.serialize_entry("source", &ser_src)?;
}
map.end()
let mut map = serializer.serialize_map(None)?;
map.serialize_entry("code", err.as_ref())?;
map.serialize_entry("msg", &format!("{err}"))?;
if let Some(src) = err.source() {
map.serialize_entry("source", &format!("{src}"))?;
}
map.end()
}
impl<E: std::error::Error> From<E> for SerializeError<E> {
fn from(err: E) -> Self {
SerializeError { err }
fn serialize_upstream_err<E, M>(err: &E, map: &mut M) -> Result<(), M::Error>
where
E: Error,
M: serde::ser::SerializeMap,
{
let src = err.source().map(|s| format!("{s}"));
map.serialize_entry("source", &src)
}
macro_rules! impl_serialize_basic {
($err_type:ident) => {
impl Serialize for $err_type {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serialize_basic_err(self, serializer)
}
}
}
}
// error during initial setup (primarily loading state from db)
#[derive(Debug, ThisError)]
#[derive(Debug, ThisError, AsRefStr)]
pub enum SetupError {
#[error("Invalid database record")]
InvalidRecord, // e.g. wrong size blob for nonce or salt
@ -52,7 +91,7 @@ pub enum SetupError {
// error when attempting to tell a request handler whether to release or deny credentials
#[derive(Debug, ThisError, IntoStaticStr)]
#[derive(Debug, ThisError, AsRefStr)]
pub enum SendResponseError {
#[error("The specified credentials request was not found")]
NotFound, // no request with the given id
@ -62,7 +101,7 @@ pub enum SendResponseError {
// errors encountered while handling an HTTP request
#[derive(Debug, ThisError)]
#[derive(Debug, ThisError, AsRefStr)]
pub enum RequestError {
#[error("Error writing to stream: {0}")]
StreamIOError(#[from] std::io::Error),
@ -82,7 +121,7 @@ pub enum RequestError {
}
#[derive(Debug, ThisError)]
#[derive(Debug, ThisError, AsRefStr)]
pub enum GetCredentialsError {
#[error("Credentials are currently locked")]
Locked,
@ -91,7 +130,7 @@ pub enum GetCredentialsError {
}
#[derive(Debug, ThisError)]
#[derive(Debug, ThisError, AsRefStr)]
pub enum GetSessionError {
#[error("Request completed successfully but no credentials were returned")]
NoCredentials, // SDK returned successfully but credentials are None
@ -100,7 +139,7 @@ pub enum GetSessionError {
}
#[derive(Debug, ThisError)]
#[derive(Debug, ThisError, AsRefStr)]
pub enum UnlockError {
#[error("App is not locked")]
NotLocked,
@ -118,10 +157,87 @@ pub enum UnlockError {
// Errors encountered while trying to figure out who's on the other end of a request
#[derive(Debug, ThisError)]
#[derive(Debug, ThisError, AsRefStr)]
pub enum ClientInfoError {
#[error("Found PID for client socket, but no corresponding process")]
ProcessNotFound,
#[error("Couldn't get client socket details: {0}")]
NetstatError(#[from] netstat2::error::Error),
}
// =========================
// Serialize implementations
// =========================
struct SerializeWrapper<E>(pub E);
impl Serialize for SerializeWrapper<&GetSessionTokenError> {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
let err = self.0;
let mut map = serializer.serialize_map(None)?;
map.serialize_entry("code", &err.code())?;
map.serialize_entry("msg", &err.message())?;
map.serialize_entry("source", &None::<&str>)?;
map.end()
}
}
impl_serialize_basic!(SetupError);
impl_serialize_basic!(SendResponseError);
impl_serialize_basic!(GetCredentialsError);
impl_serialize_basic!(ClientInfoError);
impl Serialize for RequestError {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
let mut map = serializer.serialize_map(None)?;
map.serialize_entry("code", self.as_ref())?;
map.serialize_entry("msg", &format!("{self}"))?;
match self {
RequestError::NoCredentials(src) => map.serialize_entry("source", &src)?,
RequestError::ClientInfo(src) => map.serialize_entry("source", &src)?,
_ => serialize_upstream_err(self, &mut map)?,
}
map.end()
}
}
impl Serialize for GetSessionError {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
let mut map = serializer.serialize_map(None)?;
map.serialize_entry("code", self.as_ref())?;
map.serialize_entry("msg", &format!("{self}"))?;
match self {
GetSessionError::SdkError(AwsSdkError::ServiceError(se_wrapper)) => {
let err = se_wrapper.err();
map.serialize_entry("source", &SerializeWrapper(err))?
}
_ => serialize_upstream_err(self, &mut map)?,
}
map.end()
}
}
impl Serialize for UnlockError {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
let mut map = serializer.serialize_map(None)?;
map.serialize_entry("code", self.as_ref())?;
map.serialize_entry("msg", &format!("{self}"))?;
match self {
UnlockError::GetSession(src) => map.serialize_entry("source", &src)?,
_ => serialize_upstream_err(self, &mut map)?,
}
map.end()
}
}

View File

@ -36,10 +36,8 @@ pub fn respond(response: RequestResponse, app_state: State<'_, AppState>) -> Res
#[tauri::command]
pub async fn unlock(passphrase: String, app_state: State<'_, AppState>) -> Result<(), SerializeError<UnlockError>> {
app_state.decrypt(&passphrase)
.await
.map_err(|e| SerializeError::from(e))
pub async fn unlock(passphrase: String, app_state: State<'_, AppState>) -> Result<(), UnlockError> {
app_state.decrypt(&passphrase).await
}
@ -59,10 +57,8 @@ pub async fn save_credentials(
credentials: Credentials,
passphrase: String,
app_state: State<'_, AppState>
) -> Result<(), SerializeError<UnlockError>> {
app_state.save_creds(credentials, &passphrase)
.await
.map_err(|e| SerializeError::from(e))
) -> Result<(), UnlockError> {
app_state.save_creds(credentials, &passphrase).await
}

View File

@ -1,41 +1,42 @@
<script>
import { onMount, createEventDispatcher } from 'svelte';
import { invoke } from '@tauri-apps/api/tauri';
import { getRootCause } from '../lib/errors.js';
export let appState;
const dispatch = createEventDispatcher();
let error = null;
let errorMsg = null;
let AccessKeyId, SecretAccessKey, passphrase
async function save() {
console.log('Saving credentials.');
let credentials = {AccessKeyId, SecretAccessKey};
try {
console.log('Saving credentials.');
let credentials = {AccessKeyId, SecretAccessKey};
try {
await invoke('save_credentials', {credentials, passphrase});
if (appState.currentRequest) {
dispatch('navigate', {target: 'ShowApproved'})
}
else {
dispatch('navigate', {target: 'Home'})
}
await invoke('save_credentials', {credentials, passphrase});
if (appState.currentRequest) {
dispatch('navigate', {target: 'ShowApproved'})
}
catch (e) {
error = e;
window.error = e;
else {
dispatch('navigate', {target: 'Home'})
}
}
catch (e) {
console.log("Error saving credentials:", e);
window.error = e;
if (e.code === "GetSession") {
let root = getRootCause(e);
errorMsg = `Error response from AWS (${root.code}): ${root.msg}`;
}
else {
errorMsg = e.msg;
}
}
}
</script>
{#if error}
<div class="text-red-400">{error}</div>
{#if errorMsg}
<div class="text-red-400">{errorMsg}</div>
{/if}
<form action="#" on:submit|preventDefault="{save}">

View File

@ -1,13 +1,14 @@
<script>
import { invoke } from '@tauri-apps/api/tauri';
import { createEventDispatcher } from 'svelte';
import { getRootCause } from '../lib/errors.js';
import Button from '../ui/Button.svelte';
export let appState;
const dispatch = createEventDispatcher();
let error = null;
let errorMsg = null;
let passphrase = '';
async function unlock() {
console.log('invoking unlock command.')
@ -23,14 +24,20 @@
}
}
catch (e) {
error = e;
window.error = e;
if (e.code === 'GetSession') {
let root = getRootCause(e);
errorMsg = `Error response from AWS (${root.code}): ${root.msg}`;
}
else {
errorMsg = e.msg;
}
}
}
</script>
{#if error}
<div class="text-red-400">{error}</div>
{#if errorMsg}
<div class="text-red-400">{errorMsg}</div>
{/if}
<form action="#" on:submit|preventDefault="{unlock}">