start refactoring for default credentials
This commit is contained in:
@ -42,12 +42,13 @@ CREATE TABLE credentials (
|
||||
id BLOB UNIQUE NOT NULL,
|
||||
name TEXT UNIQUE NOT NULL,
|
||||
type TEXT NOT NULL,
|
||||
is_default BOOLEAN NOT NULL,
|
||||
created_at INTEGER NOT NULL
|
||||
);
|
||||
|
||||
-- populate with basic data from existing AWS credential
|
||||
INSERT INTO credentials (id, name, type, created_at)
|
||||
SELECT id, 'default', 'aws', created_at FROM aws_tmp;
|
||||
INSERT INTO credentials (id, name, type, is_default, created_at)
|
||||
SELECT id, 'default', 'aws', 1, created_at FROM aws_tmp;
|
||||
|
||||
-- new AWS-specific table
|
||||
CREATE TABLE aws_credentials (
|
||||
|
@ -12,19 +12,27 @@ use serde::{
|
||||
};
|
||||
use serde::de::{self, Visitor};
|
||||
use sqlx::{
|
||||
SqlitePool,
|
||||
FromRow,
|
||||
Sqlite,
|
||||
Transaction,
|
||||
types::Uuid,
|
||||
};
|
||||
use sqlx::error::{
|
||||
Error as SqlxError,
|
||||
};
|
||||
use tokio_stream::StreamExt;
|
||||
|
||||
use super::{Credential, Crypto, SaveCredential, PersistentCredential};
|
||||
use super::{Crypto, PersistentCredential};
|
||||
|
||||
use crate::errors::*;
|
||||
|
||||
|
||||
#[derive(Debug, Clone, FromRow)]
|
||||
pub struct AwsRow {
|
||||
#[allow(dead_code)]
|
||||
id: Uuid,
|
||||
access_key_id: String,
|
||||
secret_key_enc: Vec<u8>,
|
||||
nonce: Vec<u8>,
|
||||
}
|
||||
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
pub struct AwsBaseCredential {
|
||||
@ -42,85 +50,123 @@ impl AwsBaseCredential {
|
||||
}
|
||||
|
||||
impl PersistentCredential for AwsBaseCredential {
|
||||
async fn save(&self, id: &Uuid, name: &str, crypto: &Crypto, pool: &SqlitePool) -> Result<(), SaveCredentialsError> {
|
||||
type Row = AwsRow;
|
||||
|
||||
fn type_name() -> &'static str { "aws" }
|
||||
|
||||
fn from_row(row: AwsRow, crypto: &Crypto) -> Result<Self, LoadCredentialsError> {
|
||||
let nonce = XNonce::clone_from_slice(&row.nonce);
|
||||
let secret_key_bytes = crypto.decrypt(&nonce, &row.secret_key_enc)?;
|
||||
let secret_key = String::from_utf8(secret_key_bytes)
|
||||
.map_err(|_| LoadCredentialsError::InvalidData)?;
|
||||
|
||||
Ok(Self::new(row.access_key_id, secret_key))
|
||||
}
|
||||
|
||||
async fn save_details(&self, id: &Uuid, crypto: &Crypto, txn: &mut Transaction<'_, Sqlite>) -> Result<(), SaveCredentialsError> {
|
||||
let (nonce, ciphertext) = crypto.encrypt(self.secret_access_key.as_bytes())?;
|
||||
let nonce_bytes = &nonce.as_slice();
|
||||
let res = sqlx::query!(
|
||||
"INSERT INTO credentials (id, name, type, created_at)
|
||||
VALUES (?, ?, 'aws', strftime('%s'))
|
||||
ON CONFLICT(id) DO UPDATE SET
|
||||
name = excluded.name,
|
||||
type = excluded.type,
|
||||
created_at = excluded.created_at;
|
||||
|
||||
INSERT OR REPLACE INTO aws_credentials (
|
||||
|
||||
sqlx::query!(
|
||||
"INSERT OR REPLACE INTO aws_credentials (
|
||||
id,
|
||||
access_key_id,
|
||||
secret_key_enc,
|
||||
nonce
|
||||
)
|
||||
VALUES (?, ?, ?, ?);",
|
||||
id,
|
||||
name,
|
||||
id, // for the second query
|
||||
self.access_key_id,
|
||||
ciphertext,
|
||||
nonce_bytes,
|
||||
).execute(pool).await;
|
||||
id, self.access_key_id, ciphertext, nonce_bytes,
|
||||
).execute(&mut **txn).await?;
|
||||
|
||||
match res {
|
||||
Err(SqlxError::Database(e)) if e.code().as_deref() == Some("2067") => Err(SaveCredentialsError::Duplicate),
|
||||
Err(e) => Err(SaveCredentialsError::DbError(e)),
|
||||
Ok(_) => Ok(())
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn load(name: &str, crypto: &Crypto, pool: &SqlitePool) -> Result<Self, LoadCredentialsError> {
|
||||
let row = sqlx::query!(
|
||||
"SELECT c.name, a.access_key_id, a.secret_key_enc, a.nonce
|
||||
FROM credentials c JOIN aws_credentials a ON a.id = c.id
|
||||
WHERE c.name = ?",
|
||||
name
|
||||
).fetch_optional(pool)
|
||||
.await?
|
||||
.ok_or(LoadCredentialsError::NoCredentials)?;
|
||||
// async fn save(&self, record: CredentialRecord, &Crypto, pool: &SqlitePool) -> Result<(), CredentialRecordsError> {
|
||||
// let (nonce, ciphertext) = crypto.encrypt(self.secret_access_key.as_bytes())?;
|
||||
// let nonce_bytes = &nonce.as_slice();
|
||||
|
||||
let nonce = XNonce::clone_from_slice(&row.nonce);
|
||||
let secret_key_bytes = crypto.decrypt(&nonce, &row.secret_key_enc)?;
|
||||
let secret_key = String::from_utf8(secret_key_bytes)
|
||||
.map_err(|_| LoadCredentialsError::InvalidData)?;
|
||||
// let res = sqlx::query!(
|
||||
// "INSERT INTO credentials (id, name, type, created_at)
|
||||
// VALUES (?, ?, 'aws', strftime('%s'))
|
||||
// ON CONFLICT(id) DO UPDATE SET
|
||||
// name = excluded.name,
|
||||
// type = excluded.type,
|
||||
// created_at = excluded.created_at;
|
||||
|
||||
// INSERT OR REPLACE INTO aws_credentials (
|
||||
// id,
|
||||
// access_key_id,
|
||||
// secret_key_enc,
|
||||
// nonce
|
||||
// )
|
||||
// VALUES (?, ?, ?, ?);",
|
||||
// id,
|
||||
// name,
|
||||
// id, // for the second query
|
||||
// self.access_key_id,
|
||||
// ciphertext,
|
||||
// nonce_bytes,
|
||||
// ).execute(pool).await;
|
||||
|
||||
Ok(AwsBaseCredential::new(row.access_key_id, secret_key))
|
||||
}
|
||||
// match res {
|
||||
// Err(SqlxError::Database(e)) if e.code().as_deref() == Some("2067") => Err(CredentialRecordsError::Duplicate),
|
||||
// Err(e) => Err(SaveCredentialsError::DbError(e)),
|
||||
// Ok(_) => Ok(())
|
||||
// }
|
||||
// }
|
||||
|
||||
async fn list(crypto: &Crypto, pool: &SqlitePool) -> Result<Vec<SaveCredential>, LoadCredentialsError> {
|
||||
let mut rows = sqlx::query!(
|
||||
"SELECT c.id, c.name, a.access_key_id, a.secret_key_enc, a.nonce
|
||||
FROM credentials c JOIN aws_credentials a ON a.id = c.id"
|
||||
).fetch(pool);
|
||||
// async fn load(name: &str, crypto: &Crypto, pool: &SqlitePool) -> Result<Self, LoadCredentialsError> {
|
||||
// let record: AwsRecord = sqlx::query_as(
|
||||
// "SELECT c.id, c.name, c.is_default, a.access_key_id, a.secret_key_enc, a.nonce
|
||||
// FROM credentials c JOIN aws_credentials a ON a.id = c.id
|
||||
// WHERE c.name = ?"
|
||||
// ).bind(name)
|
||||
// .fetch_optional(pool)
|
||||
// .await?
|
||||
// .ok_or(LoadCredentialsError::NoCredentials)?;
|
||||
|
||||
let mut creds = Vec::new();
|
||||
// let key = record.decrypt_key(crypto)?;
|
||||
// let credential = AwsBaseCredential::new(record.access_key_id, key);
|
||||
// Ok(credential)
|
||||
// }
|
||||
|
||||
while let Some(row) = rows.try_next().await? {
|
||||
let nonce = XNonce::clone_from_slice(&row.nonce);
|
||||
let secret_key_bytes = crypto.decrypt(&nonce, &row.secret_key_enc)?;
|
||||
let secret_key = String::from_utf8(secret_key_bytes)
|
||||
.map_err(|_| LoadCredentialsError::InvalidData)?;
|
||||
let aws = AwsBaseCredential::new(row.access_key_id, secret_key);
|
||||
// async fn load_default(crypto: &Crypto, pool: &SqlitePool) -> Result<Self, LoadCredentialsError> {
|
||||
// let record: AwsRecord = sqlx::query_as(
|
||||
// "SELECT c.id, c.name, c.is_default, a.access_key_id, a.secret_key_enc, a.nonce
|
||||
// FROM credentials c JOIN aws_credentials a ON a.id = c.id
|
||||
// WHERE c.type = 'aws' AND c.is_default = 1"
|
||||
// ).fetch_optional(pool)
|
||||
// .await?
|
||||
// .ok_or(LoadCredentialsError::NoCredentials)?;
|
||||
|
||||
let id = Uuid::from_slice(&row.id)
|
||||
.map_err(|_| LoadCredentialsError::InvalidData)?;
|
||||
// let key = record.decrypt_key(crypto)?;
|
||||
// let credential = AwsBaseCredential::new(record.access_key_id, key);
|
||||
// Ok(credential)
|
||||
// }
|
||||
|
||||
let cred = SaveCredential {
|
||||
id,
|
||||
name: row.name,
|
||||
credential: Credential::AwsBase(aws),
|
||||
};
|
||||
creds.push(cred);
|
||||
}
|
||||
// async fn list(crypto: &Crypto, pool: &SqlitePool) -> Result<Vec<SaveCredential>, LoadCredentialsError> {
|
||||
// let mut rows = sqlx::query_as::<_, AwsRecord>(
|
||||
// "SELECT c.id, c.name, c.is_default, a.access_key_id, a.secret_key_enc, a.nonce
|
||||
// FROM credentials c JOIN aws_credentials a ON a.id = c.id"
|
||||
// ).fetch(pool);
|
||||
|
||||
Ok(creds)
|
||||
}
|
||||
// let mut creds = Vec::new();
|
||||
|
||||
// while let Some(record) = rows.try_next().await? {
|
||||
// let key = record.decrypt_key(crypto)?;
|
||||
// let aws = AwsBaseCredential::new(record.access_key_id, key);
|
||||
|
||||
// let cred = SaveCredential {
|
||||
// id: record.id,
|
||||
// name: record.name,
|
||||
// is_default: record.is_default,
|
||||
// credential: Credential::AwsBase(aws),
|
||||
// };
|
||||
// creds.push(cred);
|
||||
// }
|
||||
|
||||
// Ok(creds)
|
||||
// }
|
||||
}
|
||||
|
||||
|
||||
@ -223,6 +269,7 @@ where S: Serializer
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
|
||||
fn test_creds() -> AwsBaseCredential {
|
||||
@ -256,7 +303,8 @@ mod tests {
|
||||
#[sqlx::test]
|
||||
async fn test_save(pool: SqlitePool) {
|
||||
let crypt = Crypto::random();
|
||||
test_creds().save(&test_uuid_random(), "test", &crypt, &pool).await
|
||||
let mut txn = pool.begin().await.unwrap();
|
||||
test_creds().save_details(&test_uuid_random(), &crypt, &mut txn).await
|
||||
.expect("Failed to save AWS credentials");
|
||||
}
|
||||
|
||||
@ -267,11 +315,12 @@ mod tests {
|
||||
|
||||
let creds = test_creds_2();
|
||||
// overwite original creds with different test data
|
||||
creds.save(&test_uuid(), "test", &crypt, &pool).await
|
||||
let mut txn = pool.begin().await.unwrap();
|
||||
creds.save_details(&test_uuid(), &crypt, &mut txn).await
|
||||
.expect("Failed to update AWS credentials");
|
||||
|
||||
// make sure update went through
|
||||
let loaded = AwsBaseCredential::load("test", &crypt, &pool).await.unwrap();
|
||||
let loaded = AwsBaseCredential::load(&test_uuid(), &crypt, &pool).await.unwrap();
|
||||
assert_eq!(creds, loaded);
|
||||
}
|
||||
|
||||
@ -281,7 +330,8 @@ mod tests {
|
||||
let crypt = Crypto::random();
|
||||
|
||||
let id = test_uuid_random();
|
||||
let resp = test_creds().save(&id, "test", &crypt, &pool).await;
|
||||
let mut txn = pool.begin().await.unwrap();
|
||||
let resp = test_creds().save_details(&id, &crypt, &mut txn).await;
|
||||
|
||||
if !matches!(resp, Err(SaveCredentialsError::Duplicate)) {
|
||||
panic!("Attempt to create duplicate entry returned {resp:?}")
|
||||
@ -292,7 +342,15 @@ mod tests {
|
||||
#[sqlx::test(fixtures("aws_credentials"))]
|
||||
async fn test_load(pool: SqlitePool) {
|
||||
let crypt = Crypto::fixed();
|
||||
let loaded = AwsBaseCredential::load("test", &crypt, &pool).await.unwrap();
|
||||
let loaded = AwsBaseCredential::load(&test_uuid(), &crypt, &pool).await.unwrap();
|
||||
assert_eq!(test_creds(), loaded);
|
||||
}
|
||||
|
||||
|
||||
#[sqlx::test(fixtures("aws_credentials"))]
|
||||
async fn test_load_by_name(pool: SqlitePool) {
|
||||
let crypt = Crypto::fixed();
|
||||
let loaded = AwsBaseCredential::load_by_name("test", &crypt, &pool).await.unwrap();
|
||||
assert_eq!(test_creds(), loaded);
|
||||
}
|
||||
|
||||
@ -301,45 +359,48 @@ mod tests {
|
||||
async fn test_save_load(pool: SqlitePool) {
|
||||
let crypt = Crypto::random();
|
||||
let creds = test_creds();
|
||||
creds.save(&test_uuid_random(), "test", &crypt, &pool).await.unwrap();
|
||||
let loaded = AwsBaseCredential::load("test", &crypt, &pool).await.unwrap();
|
||||
let id = test_uuid_random();
|
||||
|
||||
let mut txn = pool.begin().await.unwrap();
|
||||
creds.save_details(&id, &crypt, &mut txn).await.unwrap();
|
||||
let loaded = AwsBaseCredential::load(&id, &crypt, &pool).await.unwrap();
|
||||
|
||||
assert_eq!(creds, loaded);
|
||||
}
|
||||
|
||||
#[sqlx::test(fixtures("aws_credentials"))]
|
||||
async fn test_list(pool: SqlitePool) {
|
||||
let crypt = Crypto::fixed();
|
||||
let list = AwsBaseCredential::list(&crypt, &pool).await
|
||||
.expect("Failed to list AWS credentials");
|
||||
// #[sqlx::test(fixtures("aws_credentials"))]
|
||||
// async fn test_list(pool: SqlitePool) {
|
||||
// let crypt = Crypto::fixed();
|
||||
// let list = AwsBaseCredential::list(&crypt, &pool).await
|
||||
// .expect("Failed to list AWS credentials");
|
||||
|
||||
let first = SaveCredential {
|
||||
id: test_uuid(),
|
||||
name: "test".into(),
|
||||
credential: Credential::AwsBase(test_creds()),
|
||||
};
|
||||
assert_eq!(&first, &list[0]);
|
||||
// let first = SaveCredential {
|
||||
// id: test_uuid(),
|
||||
// name: "test".into(),
|
||||
// credential: Credential::AwsBase(test_creds()),
|
||||
// };
|
||||
// assert_eq!(&first, &list[0]);
|
||||
|
||||
let second = SaveCredential {
|
||||
id: test_uuid_2(),
|
||||
name: "test2".into(),
|
||||
credential: Credential::AwsBase(test_creds_2()),
|
||||
};
|
||||
assert_eq!(&second, &list[1]);
|
||||
}
|
||||
// let second = SaveCredential {
|
||||
// id: test_uuid_2(),
|
||||
// name: "test2".into(),
|
||||
// credential: Credential::AwsBase(test_creds_2()),
|
||||
// };
|
||||
// assert_eq!(&second, &list[1]);
|
||||
// }
|
||||
|
||||
#[sqlx::test(fixtures("aws_credentials"))]
|
||||
async fn test_rekey(pool: SqlitePool) {
|
||||
let old_crypt = Crypto::fixed();
|
||||
let orig = AwsBaseCredential::list(&old_crypt, &pool).await.unwrap();
|
||||
// #[sqlx::test(fixtures("aws_credentials"))]
|
||||
// async fn test_rekey(pool: SqlitePool) {
|
||||
// let old_crypt = Crypto::fixed();
|
||||
// let orig = AwsBaseCredential::list(&old_crypt, &pool).await.unwrap();
|
||||
|
||||
let new_crypt = Crypto::random();
|
||||
AwsBaseCredential::rekey(&old_crypt, &new_crypt, &pool).await
|
||||
.expect("Failed to re-key AWS credentials");
|
||||
// let new_crypt = Crypto::random();
|
||||
// AwsBaseCredential::rekey(&old_crypt, &new_crypt, &pool).await
|
||||
// .expect("Failed to re-key AWS credentials");
|
||||
|
||||
let rekeyed = AwsBaseCredential::list(&new_crypt, &pool).await.unwrap();
|
||||
for (before, after) in orig.iter().zip(rekeyed.iter()) {
|
||||
assert_eq!(before, after);
|
||||
}
|
||||
}
|
||||
// let rekeyed = AwsBaseCredential::list(&new_crypt, &pool).await.unwrap();
|
||||
// for (before, after) in orig.iter().zip(rekeyed.iter()) {
|
||||
// assert_eq!(before, after);
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
INSERT INTO credentials (id, name, type, created_at)
|
||||
INSERT INTO credentials (id, name, type, is_default, created_at)
|
||||
VALUES
|
||||
(X'00000000000000000000000000000000', 'test', 'aws', strftime('%s')),
|
||||
(X'ffffffffffffffffffffffffffffffff', 'test2', 'aws', strftime('%s'));
|
||||
(X'00000000000000000000000000000000', 'test', 'aws', strftime('%s'), 1),
|
||||
(X'ffffffffffffffffffffffffffffffff', 'test2', 'aws', strftime('%s'), 0);
|
||||
|
||||
INSERT INTO aws_credentials (id, access_key_id, secret_key_enc, nonce)
|
||||
VALUES
|
||||
|
@ -24,8 +24,15 @@ use serde::{
|
||||
Deserializer,
|
||||
};
|
||||
use serde::de::{self, Visitor};
|
||||
use sqlx::SqlitePool;
|
||||
use sqlx::types::Uuid;
|
||||
use sqlx::{
|
||||
Error as SqlxError,
|
||||
FromRow,
|
||||
Sqlite,
|
||||
SqlitePool,
|
||||
sqlite::SqliteRow,
|
||||
Transaction,
|
||||
types::Uuid,
|
||||
};
|
||||
|
||||
use crate::errors::*;
|
||||
use crate::kv;
|
||||
@ -35,6 +42,7 @@ pub use aws::{AwsBaseCredential, AwsSessionCredential};
|
||||
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum Credential {
|
||||
AwsBase(AwsBaseCredential),
|
||||
AwsSession(AwsSessionCredential),
|
||||
@ -43,27 +51,74 @@ pub enum Credential {
|
||||
|
||||
// we need a special type for listing structs because
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
|
||||
pub struct SaveCredential {
|
||||
pub struct CredentialRecord {
|
||||
#[serde(serialize_with = "serialize_uuid")]
|
||||
#[serde(deserialize_with = "deserialize_uuid")]
|
||||
id: Uuid, // UUID so it can be generated on the frontend
|
||||
name: String, // user-facing identifier so it can be changed
|
||||
is_default: bool,
|
||||
credential: Credential,
|
||||
}
|
||||
|
||||
impl SaveCredential {
|
||||
pub async fn save(&self, crypt: &Crypto, pool: &SqlitePool) -> Result<(), SaveCredentialsError> {
|
||||
let cred = match &self.credential {
|
||||
Credential::AwsBase(b) => b,
|
||||
Credential::AwsSession(_) => return Err(SaveCredentialsError::NotPersistent),
|
||||
impl CredentialRecord {
|
||||
pub async fn save(&self, crypto: &Crypto, pool: &SqlitePool) -> Result<(), SaveCredentialsError> {
|
||||
let type_name = match &self.credential {
|
||||
Credential::AwsBase(_) => AwsBaseCredential::type_name(),
|
||||
_ => return Err(SaveCredentialsError::NotPersistent),
|
||||
};
|
||||
|
||||
cred.save(&self.id, &self.name, crypt, pool).await
|
||||
// if the credential being saved is default, make sure it's the only default of its type
|
||||
let mut txn = pool.begin().await?;
|
||||
if self.is_default {
|
||||
sqlx::query!(
|
||||
"UPDATE credentials SET is_default = 0 WHERE type = ?",
|
||||
type_name
|
||||
).execute(&mut *txn).await?;
|
||||
}
|
||||
|
||||
// save to parent credentials table
|
||||
let res = sqlx::query!(
|
||||
"INSERT INTO credentials (id, name, type, is_default)
|
||||
VALUES (?, ?, ?, ?)
|
||||
ON CONFLICT DO UPDATE SET
|
||||
name = excluded.name,
|
||||
type = excluded.type,
|
||||
is_default = excluded.is_default",
|
||||
self.id, self.name, type_name, self.is_default
|
||||
).execute(&mut *txn).await;
|
||||
|
||||
// if id is unique, but name is not, we will get an error
|
||||
// (if id is not unique, this becomes an upsert due to ON CONFLICT clause)
|
||||
match res {
|
||||
Err(SqlxError::Database(e)) if e.is_unique_violation() => Err(SaveCredentialsError::Duplicate),
|
||||
Err(e) => Err(SaveCredentialsError::DbError(e)),
|
||||
Ok(_) => Ok(())
|
||||
}?;
|
||||
|
||||
// save credential details to child table
|
||||
match &self.credential {
|
||||
Credential::AwsBase(b) => b.save_details(&self.id, crypto, &mut txn).await,
|
||||
_ => Err(SaveCredentialsError::NotPersistent),
|
||||
}?;
|
||||
|
||||
// make it real
|
||||
txn.commit().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(unused_variables)]
|
||||
pub async fn list(crypto: &Crypto, pool: &SqlitePool) -> Result<Vec<Self>, LoadCredentialsError> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
#[allow(unused_variables)]
|
||||
pub async fn rekey(old: &Crypto, new: &Crypto, pool: &SqlitePool) -> Result<(), SaveCredentialsError> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
fn serialize_uuid<S: Serializer>(u: &Uuid, s: S) -> Result<S::Ok, S::Error> {
|
||||
let mut buf = Vec::new();
|
||||
let mut buf = Uuid::encode_buffer();
|
||||
s.serialize_str(u.as_hyphenated().encode_lower(&mut buf))
|
||||
}
|
||||
|
||||
@ -88,15 +143,57 @@ fn deserialize_uuid<'de, D: Deserializer<'de>>(ds: D) -> Result<Uuid, D::Error>
|
||||
|
||||
|
||||
pub trait PersistentCredential: for<'a> Deserialize<'a> + Sized {
|
||||
async fn load(name: &str, crypt: &Crypto, pool: &SqlitePool) -> Result<Self, LoadCredentialsError>;
|
||||
async fn list(crypt: &Crypto, pool: &SqlitePool) -> Result<Vec<SaveCredential>, LoadCredentialsError>;
|
||||
async fn save(&self, id: &Uuid, name: &str, crypt: &Crypto, pool: &SqlitePool) -> Result<(), SaveCredentialsError>;
|
||||
type Row: Send + Unpin + for<'r> FromRow<'r, SqliteRow>;
|
||||
|
||||
async fn rekey(old: &Crypto, new: &Crypto, pool: &SqlitePool) -> Result<(), SaveCredentialsError> {
|
||||
for cred in Self::list(old, pool).await? {
|
||||
cred.save(new, pool).await?;
|
||||
}
|
||||
Ok(())
|
||||
fn type_name() -> &'static str;
|
||||
fn from_row(row: Self::Row, crypto: &Crypto) -> Result<Self, LoadCredentialsError>;
|
||||
// save_details needs to be implemented per-type because we don't know the number of parameters in advance
|
||||
async fn save_details(&self, id: &Uuid, crypto: &Crypto, txn: &mut Transaction<'_, Sqlite>) -> Result<(), SaveCredentialsError>;
|
||||
|
||||
fn table_name() -> String {
|
||||
format!("{}_credentials", Self::type_name())
|
||||
}
|
||||
|
||||
async fn load(id: &Uuid, crypto: &Crypto, pool: &SqlitePool) -> Result<Self, LoadCredentialsError> {
|
||||
let q = format!("SELECT * FROM {} WHERE id = ?", Self::table_name());
|
||||
let row: Self::Row = sqlx::query_as(&q)
|
||||
.bind(id)
|
||||
.fetch_optional(pool)
|
||||
.await?
|
||||
.ok_or(LoadCredentialsError::NoCredentials)?;
|
||||
|
||||
Self::from_row(row, crypto)
|
||||
}
|
||||
|
||||
async fn load_by_name(name: &str, crypto: &Crypto, pool: &SqlitePool) -> Result<Self, LoadCredentialsError> {
|
||||
let q = format!(
|
||||
"SELECT * FROM {} WHERE id = (SELECT id FROM credentials WHERE name = ?)",
|
||||
Self::table_name(),
|
||||
);
|
||||
let row: Self::Row = sqlx::query_as(&q)
|
||||
.bind(name)
|
||||
.fetch_optional(pool)
|
||||
.await?
|
||||
.ok_or(LoadCredentialsError::NoCredentials)?;
|
||||
|
||||
Self::from_row(row, crypto)
|
||||
}
|
||||
|
||||
async fn load_default(crypto: &Crypto, pool: &SqlitePool) -> Result<Self, LoadCredentialsError> {
|
||||
let q = format!(
|
||||
"SELECT details.*
|
||||
FROM {} details
|
||||
JOIN credentials c
|
||||
ON c.id = details.id
|
||||
AND c.is_default = 1",
|
||||
Self::table_name(),
|
||||
);
|
||||
let row: Self::Row = sqlx::query_as(&q)
|
||||
.fetch_optional(pool)
|
||||
.await?
|
||||
.ok_or(LoadCredentialsError::NoCredentials)?;
|
||||
|
||||
Self::from_row(row, crypto)
|
||||
}
|
||||
}
|
||||
|
||||
@ -300,14 +397,48 @@ impl Debug for Crypto {
|
||||
}
|
||||
|
||||
|
||||
// #[cfg(test)]
|
||||
// mod tests {
|
||||
// use super::*;
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
// #[sqlx::test(fixtures("uuid_test"))]
|
||||
// async fn save_uuid(pool: SqlitePool) {
|
||||
// let u = Uuid::try_parse("7140b90c-bfbd-4394-9008-01b94f94ecf8").unwrap();
|
||||
// sqlx::query!("INSERT INTO uuids (uuid) VALUES (?)", u).execute(pool).unwrap();
|
||||
// panic!("done, go check db");
|
||||
// }
|
||||
// }
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)]
|
||||
struct UuidWrapper {
|
||||
#[serde(serialize_with = "serialize_uuid")]
|
||||
#[serde(deserialize_with = "deserialize_uuid")]
|
||||
id: Uuid,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_serialize_uuid() {
|
||||
let u = UuidWrapper {
|
||||
id: Uuid::try_parse("693f84d2-4c1b-41e5-8483-cbe178324e04").unwrap()
|
||||
};
|
||||
let computed = serde_json::to_string(&u).unwrap();
|
||||
assert_eq!(
|
||||
"{\"id\":\"693f84d2-4c1b-41e5-8483-cbe178324e04\"}",
|
||||
&computed,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deserialize_uuid() {
|
||||
let s = "{\"id\":\"045bd359-8630-4b76-9b7d-e4a86ed2222c\"}";
|
||||
let computed = serde_json::from_str(s).unwrap();
|
||||
let expected = UuidWrapper {
|
||||
id: Uuid::try_parse("045bd359-8630-4b76-9b7d-e4a86ed2222c").unwrap(),
|
||||
};
|
||||
assert_eq!(expected, computed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_serialize_deserialize_uuid() {
|
||||
let buf = Crypto::salt();
|
||||
let expected = UuidWrapper{
|
||||
id: Uuid::from_slice(&buf[..16]).unwrap()
|
||||
};
|
||||
let serialized = serde_json::to_string(&expected).unwrap();
|
||||
let computed = serde_json::from_str(&serialized).unwrap();
|
||||
assert_eq!(expected, computed)
|
||||
}
|
||||
}
|
||||
|
@ -5,7 +5,7 @@ use tauri::State;
|
||||
use crate::config::AppConfig;
|
||||
use crate::credentials::{
|
||||
AppSession,
|
||||
SaveCredential
|
||||
CredentialRecord
|
||||
};
|
||||
use crate::errors::*;
|
||||
use crate::clientinfo::Client;
|
||||
@ -107,10 +107,10 @@ pub async fn signal_activity(app_state: State<'_, AppState>) -> Result<(), ()> {
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn save_credential(
|
||||
cred: SaveCredential,
|
||||
record: CredentialRecord,
|
||||
app_state: State<'_, AppState>
|
||||
) -> Result<(), SaveCredentialsError> {
|
||||
app_state.save_credential(cred).await
|
||||
app_state.save_credential(record).await
|
||||
}
|
||||
|
||||
|
||||
@ -123,7 +123,7 @@ pub async fn delete_credential(id: &str, app_state: State<'_, AppState>) -> Resu
|
||||
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn list_credentials(app_state: State<'_, AppState>) -> Result<Vec<SaveCredential>, GetCredentialsError> {
|
||||
pub async fn list_credentials(app_state: State<'_, AppState>) -> Result<Vec<CredentialRecord>, GetCredentialsError> {
|
||||
app_state.list_credentials().await
|
||||
}
|
||||
|
||||
|
@ -21,7 +21,7 @@ use crate::credentials::{
|
||||
use crate::{config, config::AppConfig};
|
||||
use crate::credentials::{
|
||||
AwsBaseCredential,
|
||||
SaveCredential,
|
||||
CredentialRecord,
|
||||
PersistentCredential
|
||||
};
|
||||
use crate::ipc::{self, RequestResponse};
|
||||
@ -142,10 +142,10 @@ impl AppState {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn save_credential(&self, cred: SaveCredential) -> Result<(), SaveCredentialsError> {
|
||||
pub async fn save_credential(&self, record: CredentialRecord) -> Result<(), SaveCredentialsError> {
|
||||
let session = self.app_session.read().await;
|
||||
let crypto = session.try_get_crypto()?;
|
||||
cred.save(crypto, &self.pool).await
|
||||
record.save(crypto, &self.pool).await
|
||||
}
|
||||
|
||||
pub async fn delete_credential(&self, id: &Uuid) -> Result<(), SaveCredentialsError> {
|
||||
@ -155,12 +155,11 @@ impl AppState {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn list_credentials(&self) -> Result<Vec<SaveCredential>, GetCredentialsError> {
|
||||
pub async fn list_credentials(&self) -> Result<Vec<CredentialRecord>, GetCredentialsError> {
|
||||
let session = self.app_session.read().await;
|
||||
let crypto = session.try_get_crypto()?;
|
||||
let creds = AwsBaseCredential::list(crypto, &self.pool).await?;
|
||||
// eventual extend this vec with other credential types
|
||||
Ok(creds)
|
||||
let list = CredentialRecord::list(crypto, &self.pool).await?;
|
||||
Ok(list)
|
||||
}
|
||||
|
||||
pub async fn set_passphrase(&self, passphrase: &str) -> Result<(), SaveCredentialsError> {
|
||||
@ -171,7 +170,7 @@ impl AppState {
|
||||
|
||||
let new_session = AppSession::new(passphrase)?;
|
||||
if let AppSession::Unlocked {salt: _, ref crypto} = *cur_session {
|
||||
AwsBaseCredential::rekey(
|
||||
CredentialRecord::rekey(
|
||||
crypto,
|
||||
new_session.try_get_crypto().expect("AppSession::new() should always return Unlocked"),
|
||||
&self.pool,
|
||||
@ -258,7 +257,7 @@ impl AppState {
|
||||
pub async fn get_aws_base(&self, name: &str) -> Result<AwsBaseCredential, GetCredentialsError> {
|
||||
let app_session = self.app_session.read().await;
|
||||
let crypto = app_session.try_get_crypto()?;
|
||||
let creds = AwsBaseCredential::load(name, crypto, &self.pool).await?;
|
||||
let creds = AwsBaseCredential::load_by_name(name, crypto, &self.pool).await?;
|
||||
Ok(creds)
|
||||
}
|
||||
|
||||
@ -345,12 +344,7 @@ mod tests {
|
||||
state.delete_credential(&id).await.unwrap();
|
||||
|
||||
// ensure delete-cascade went through correctly
|
||||
let res = AwsBaseCredential::load(
|
||||
"test",
|
||||
&Crypto::fixed(),
|
||||
&state.pool,
|
||||
).await;
|
||||
|
||||
let res = AwsBaseCredential::load(&id, &Crypto::fixed(), &state.pool).await;
|
||||
assert!(matches!(res, Err(LoadCredentialsError::NoCredentials)));
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user