continue working on default credentials
This commit is contained in:
parent
ce7d75f15a
commit
bb980c5eef
@ -18,14 +18,14 @@ use sqlx::{
|
|||||||
types::Uuid,
|
types::Uuid,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{Crypto, PersistentCredential};
|
use super::{Credential, Crypto, PersistentCredential};
|
||||||
|
|
||||||
use crate::errors::*;
|
use crate::errors::*;
|
||||||
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, FromRow)]
|
#[derive(Debug, Clone, FromRow)]
|
||||||
pub struct AwsRow {
|
pub struct AwsRow {
|
||||||
pub id: Uuid,
|
id: Uuid,
|
||||||
access_key_id: String,
|
access_key_id: String,
|
||||||
secret_key_enc: Vec<u8>,
|
secret_key_enc: Vec<u8>,
|
||||||
nonce: Vec<u8>,
|
nonce: Vec<u8>,
|
||||||
@ -53,6 +53,10 @@ impl PersistentCredential for AwsBaseCredential {
|
|||||||
|
|
||||||
fn type_name() -> &'static str { "aws" }
|
fn type_name() -> &'static str { "aws" }
|
||||||
|
|
||||||
|
fn into_credential(self) -> Credential { Credential::AwsBase(self) }
|
||||||
|
|
||||||
|
fn row_id(row: &AwsRow) -> Uuid { row.id }
|
||||||
|
|
||||||
fn from_row(row: AwsRow, crypto: &Crypto) -> Result<Self, LoadCredentialsError> {
|
fn from_row(row: AwsRow, crypto: &Crypto) -> Result<Self, LoadCredentialsError> {
|
||||||
let nonce = XNonce::clone_from_slice(&row.nonce);
|
let nonce = XNonce::clone_from_slice(&row.nonce);
|
||||||
let secret_key_bytes = crypto.decrypt(&nonce, &row.secret_key_enc)?;
|
let secret_key_bytes = crypto.decrypt(&nonce, &row.secret_key_enc)?;
|
||||||
@ -79,93 +83,6 @@ impl PersistentCredential for AwsBaseCredential {
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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 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;
|
|
||||||
|
|
||||||
// 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 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 key = record.decrypt_key(crypto)?;
|
|
||||||
// let credential = AwsBaseCredential::new(record.access_key_id, key);
|
|
||||||
// Ok(credential)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// 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 key = record.decrypt_key(crypto)?;
|
|
||||||
// let credential = AwsBaseCredential::new(record.access_key_id, key);
|
|
||||||
// Ok(credential)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// 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);
|
|
||||||
|
|
||||||
// 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)
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -269,6 +186,7 @@ where S: Serializer
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use sqlx::SqlitePool;
|
use sqlx::SqlitePool;
|
||||||
|
use sqlx::types::uuid::uuid;
|
||||||
|
|
||||||
|
|
||||||
fn creds() -> AwsBaseCredential {
|
fn creds() -> AwsBaseCredential {
|
||||||
@ -302,7 +220,8 @@ mod tests {
|
|||||||
#[sqlx::test(fixtures("aws_credentials"))]
|
#[sqlx::test(fixtures("aws_credentials"))]
|
||||||
async fn test_load(pool: SqlitePool) {
|
async fn test_load(pool: SqlitePool) {
|
||||||
let crypt = Crypto::fixed();
|
let crypt = Crypto::fixed();
|
||||||
let loaded = AwsBaseCredential::load(&test_uuid(), &crypt, &pool).await.unwrap();
|
let id = uuid!("00000000-0000-0000-0000-000000000000");
|
||||||
|
let loaded = AwsBaseCredential::load(&id, &crypt, &pool).await.unwrap();
|
||||||
assert_eq!(creds(), loaded);
|
assert_eq!(creds(), loaded);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -326,14 +245,14 @@ mod tests {
|
|||||||
#[sqlx::test(fixtures("aws_credentials"))]
|
#[sqlx::test(fixtures("aws_credentials"))]
|
||||||
async fn test_list(pool: SqlitePool) {
|
async fn test_list(pool: SqlitePool) {
|
||||||
let crypt = Crypto::fixed();
|
let crypt = Crypto::fixed();
|
||||||
let list: Vec<_> = AwsBaseCredential::list(&pool)
|
let list: Vec<_> = AwsBaseCredential::list(&crypt, &pool)
|
||||||
.await
|
.await
|
||||||
.expect("Failed to load credentials")
|
.expect("Failed to load credentials")
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|r| AwsBaseCredential::from_row(r, &crypt).unwrap())
|
.map(|(_, cred)| cred)
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
assert_eq!(&creds(), &list[0]);
|
assert_eq!(&creds().into_credential(), &list[0]);
|
||||||
assert_eq!(&creds_2(), &list[1]);
|
assert_eq!(&creds_2().into_credential(), &list[1]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
use std::fmt::Formatter;
|
|
||||||
|
|
||||||
use serde::{Serialize, Deserialize};
|
use serde::{Serialize, Deserialize};
|
||||||
use sqlx::{
|
use sqlx::{
|
||||||
FromRow,
|
FromRow,
|
||||||
@ -9,6 +7,7 @@ use sqlx::{
|
|||||||
Transaction,
|
Transaction,
|
||||||
types::Uuid,
|
types::Uuid,
|
||||||
};
|
};
|
||||||
|
use tokio_stream::StreamExt;
|
||||||
|
|
||||||
use crate::errors::*;
|
use crate::errors::*;
|
||||||
|
|
||||||
@ -37,7 +36,13 @@ pub trait PersistentCredential: for<'a> Deserialize<'a> + Sized {
|
|||||||
type Row: Send + Unpin + for<'r> FromRow<'r, SqliteRow>;
|
type Row: Send + Unpin + for<'r> FromRow<'r, SqliteRow>;
|
||||||
|
|
||||||
fn type_name() -> &'static str;
|
fn type_name() -> &'static str;
|
||||||
|
|
||||||
|
fn into_credential(self) -> Credential;
|
||||||
|
|
||||||
|
fn row_id(row: &Self::Row) -> Uuid;
|
||||||
|
|
||||||
fn from_row(row: Self::Row, crypto: &Crypto) -> Result<Self, LoadCredentialsError>;
|
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
|
// 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>;
|
async fn save_details(&self, id: &Uuid, crypto: &Crypto, txn: &mut Transaction<'_, Sqlite>) -> Result<(), SaveCredentialsError>;
|
||||||
|
|
||||||
@ -87,9 +92,25 @@ pub trait PersistentCredential: for<'a> Deserialize<'a> + Sized {
|
|||||||
Self::from_row(row, crypto)
|
Self::from_row(row, crypto)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn list(pool: &SqlitePool) -> Result<Vec<Self::Row>, LoadCredentialsError> {
|
async fn list(crypto: &Crypto, pool: &SqlitePool) -> Result<Vec<(Uuid, Credential)>, LoadCredentialsError> {
|
||||||
let q = format!("SELECT * FROM {}", Self::table_name());
|
let q = format!(
|
||||||
let rows: Vec<Self::Row> = sqlx::query_as(&q).fetch_all(pool).await?;
|
"SELECT details.*
|
||||||
Ok(rows)
|
FROM
|
||||||
|
{} details
|
||||||
|
JOIN credentials c
|
||||||
|
ON c.id = details.id
|
||||||
|
ORDER BY c.created_at",
|
||||||
|
Self::table_name(),
|
||||||
|
);
|
||||||
|
let mut rows = sqlx::query_as::<_, Self::Row>(&q).fetch(pool);
|
||||||
|
|
||||||
|
let mut creds = Vec::new();
|
||||||
|
while let Some(row) = rows.try_next().await? {
|
||||||
|
let id = Self::row_id(&row);
|
||||||
|
let cred = Self::from_row(row, crypto)?.into_credential();
|
||||||
|
creds.push((id, cred));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(creds)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -18,7 +18,6 @@ use tokio_stream::StreamExt;
|
|||||||
use crate::errors::*;
|
use crate::errors::*;
|
||||||
use super::{
|
use super::{
|
||||||
AwsBaseCredential,
|
AwsBaseCredential,
|
||||||
aws::AwsRow,
|
|
||||||
Credential,
|
Credential,
|
||||||
Crypto,
|
Crypto,
|
||||||
PersistentCredential,
|
PersistentCredential,
|
||||||
@ -100,7 +99,7 @@ impl CredentialRecord {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn load_details(row: CredentialRow, crypto: &Crypto, pool: &SqlitePool) -> Result<Self, LoadCredentialsError> {
|
async fn load_credential(row: CredentialRow, crypto: &Crypto, pool: &SqlitePool) -> Result<Self, LoadCredentialsError> {
|
||||||
let credential = match row.credential_type.as_str() {
|
let credential = match row.credential_type.as_str() {
|
||||||
"aws" => Credential::AwsBase(AwsBaseCredential::load(&row.id, crypto, pool).await?),
|
"aws" => Credential::AwsBase(AwsBaseCredential::load(&row.id, crypto, pool).await?),
|
||||||
_ => return Err(LoadCredentialsError::InvalidData),
|
_ => return Err(LoadCredentialsError::InvalidData),
|
||||||
@ -116,7 +115,7 @@ impl CredentialRecord {
|
|||||||
.await?
|
.await?
|
||||||
.ok_or(LoadCredentialsError::NoCredentials)?;
|
.ok_or(LoadCredentialsError::NoCredentials)?;
|
||||||
|
|
||||||
Self::load_details(row, crypto, pool).await
|
Self::load_credential(row, crypto, pool).await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn load_default(credential_type: &str, crypto: &Crypto, pool: &SqlitePool) -> Result<Self, LoadCredentialsError> {
|
pub async fn load_default(credential_type: &str, crypto: &Crypto, pool: &SqlitePool) -> Result<Self, LoadCredentialsError> {
|
||||||
@ -128,7 +127,7 @@ impl CredentialRecord {
|
|||||||
.await?
|
.await?
|
||||||
.ok_or(LoadCredentialsError::NoCredentials)?;
|
.ok_or(LoadCredentialsError::NoCredentials)?;
|
||||||
|
|
||||||
Self::load_details(row, crypto, pool).await
|
Self::load_credential(row, crypto, pool).await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn list(crypto: &Crypto, pool: &SqlitePool) -> Result<Vec<Self>, LoadCredentialsError> {
|
pub async fn list(crypto: &Crypto, pool: &SqlitePool) -> Result<Vec<Self>, LoadCredentialsError> {
|
||||||
@ -143,10 +142,9 @@ impl CredentialRecord {
|
|||||||
|
|
||||||
let mut records = Vec::with_capacity(parent_map.len());
|
let mut records = Vec::with_capacity(parent_map.len());
|
||||||
|
|
||||||
for row in AwsBaseCredential::list(&pool).await? {
|
for (id, credential) in AwsBaseCredential::list(crypto, pool).await? {
|
||||||
let parent = parent_map.remove(&row.id)
|
let parent = parent_map.remove(&id)
|
||||||
.ok_or(LoadCredentialsError::InvalidData)?;
|
.ok_or(LoadCredentialsError::InvalidData)?;
|
||||||
let credential = Credential::AwsBase(AwsBaseCredential::from_row(row, crypto)?);
|
|
||||||
records.push(Self::from_parts(parent, credential));
|
records.push(Self::from_parts(parent, credential));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -274,6 +272,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[sqlx::test]
|
||||||
async fn test_overwrite_aws(pool: SqlitePool) {
|
async fn test_overwrite_aws(pool: SqlitePool) {
|
||||||
let crypt = Crypto::fixed();
|
let crypt = Crypto::fixed();
|
||||||
|
|
||||||
@ -329,6 +328,18 @@ mod tests {
|
|||||||
.expect("Failed to load other credential");
|
.expect("Failed to load other credential");
|
||||||
assert!(!other_loaded.is_default);
|
assert!(!other_loaded.is_default);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[sqlx::test(fixtures("aws_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!(aws_record(), records[0]);
|
||||||
|
assert_eq!(aws_record_2(), records[1]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
<script>
|
<script>
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { slide, fade } from 'svelte/transition';
|
import { slide, fade } from 'svelte/transition';
|
||||||
|
import { writable } from 'svelte/store';
|
||||||
import { invoke } from '@tauri-apps/api/core';
|
import { invoke } from '@tauri-apps/api/core';
|
||||||
|
|
||||||
import AwsCredential from './credentials/AwsCredential.svelte';
|
import AwsCredential from './credentials/AwsCredential.svelte';
|
||||||
@ -10,14 +11,15 @@
|
|||||||
let show = false;
|
let show = false;
|
||||||
|
|
||||||
let records = []
|
let records = []
|
||||||
|
let defaults = writable({});
|
||||||
async function loadCreds() {
|
async function loadCreds() {
|
||||||
records = await invoke('list_credentials');
|
records = await invoke('list_credentials');
|
||||||
console.log(records);
|
let pairs = records.filter(r => r.is_default).map(r => [r.credential.type, r.id]);
|
||||||
|
$defaults = Object.fromEntries(pairs);
|
||||||
}
|
}
|
||||||
onMount(loadCreds);
|
onMount(loadCreds);
|
||||||
|
|
||||||
function newCred() {
|
function newCred() {
|
||||||
console.log('hello!');
|
|
||||||
records.push({
|
records.push({
|
||||||
id: crypto.randomUUID(),
|
id: crypto.randomUUID(),
|
||||||
name: '',
|
name: '',
|
||||||
@ -42,7 +44,7 @@
|
|||||||
{#if records.length > 0}
|
{#if records.length > 0}
|
||||||
<div class="rounded-box border-2 border-neutral-content/30 divide-y-2 divide-neutral-content/30">
|
<div class="rounded-box border-2 border-neutral-content/30 divide-y-2 divide-neutral-content/30">
|
||||||
{#each records as record (record.id)}
|
{#each records as record (record.id)}
|
||||||
<AwsCredential {record} on:update={loadCreds} />
|
<AwsCredential {record} {defaults} on:update={loadCreds} />
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-primary btn-wide mx-auto" on:click={newCred}>
|
<button class="btn btn-primary btn-wide mx-auto" on:click={newCred}>
|
||||||
|
@ -6,25 +6,29 @@
|
|||||||
import ErrorAlert from '../../ui/ErrorAlert.svelte';
|
import ErrorAlert from '../../ui/ErrorAlert.svelte';
|
||||||
import Icon from '../../ui/Icon.svelte';
|
import Icon from '../../ui/Icon.svelte';
|
||||||
|
|
||||||
export let record
|
export let record;
|
||||||
|
export let defaults;
|
||||||
|
|
||||||
import PassphraseInput from '../../ui/PassphraseInput.svelte';
|
import PassphraseInput from '../../ui/PassphraseInput.svelte';
|
||||||
|
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
// if record.credential is blank when component is first instantiated, this is
|
|
||||||
// a newly-added credential, so show details so that data can be filled out
|
|
||||||
let showDetails = record.isNew ? true : false;
|
let showDetails = record.isNew ? true : false;
|
||||||
|
|
||||||
let localName = name;
|
let localName = name;
|
||||||
let local = JSON.parse(JSON.stringify(record));
|
let local = JSON.parse(JSON.stringify(record));
|
||||||
$: isModified = JSON.stringify(local) !== JSON.stringify(record);
|
$: isModified = JSON.stringify(local) !== JSON.stringify(record);
|
||||||
|
|
||||||
|
// explicitly subscribe to updates to `default`, so that we can update
|
||||||
|
// our local copy even if the component hasn't been recreated
|
||||||
|
// (sadly we can't use a reactive binding because reasons I guess)
|
||||||
|
defaults.subscribe(d => local.is_default = local.id === d[local.credential.type])
|
||||||
|
|
||||||
let error, alert;
|
let error, alert;
|
||||||
async function saveCredential() {
|
async function saveCredential() {
|
||||||
try {
|
try {
|
||||||
await invoke('save_credential', {cred: local});
|
await invoke('save_credential', {record: local});
|
||||||
dispatch('update');
|
dispatch('update');
|
||||||
showDetails = false;
|
showDetails = false;
|
||||||
}
|
}
|
||||||
@ -34,11 +38,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let deleteModal;
|
||||||
let confirmDelete;
|
|
||||||
function conditionalDelete() {
|
function conditionalDelete() {
|
||||||
if (!record.isNew) {
|
if (!record.isNew) {
|
||||||
confirmDelete.showModal();
|
deleteModal.showModal();
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
deleteCredential();
|
deleteCredential();
|
||||||
@ -139,7 +142,7 @@
|
|||||||
</form>
|
</form>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<dialog bind:this={confirmDelete} class="modal">
|
<dialog bind:this={deleteModal} class="modal">
|
||||||
<div class="modal-box">
|
<div class="modal-box">
|
||||||
<h3 class="text-lg font-bold">Delete AWS credential "{record.name}"?</h3>
|
<h3 class="text-lg font-bold">Delete AWS credential "{record.name}"?</h3>
|
||||||
<div class="modal-action">
|
<div class="modal-action">
|
||||||
|
Loading…
x
Reference in New Issue
Block a user