Skip to content

Commit

Permalink
feat: add a local secret store to the Leaf RPC server.
Browse files Browse the repository at this point in the history
  • Loading branch information
zicklag committed Sep 13, 2024
1 parent aa5f44f commit e652393
Show file tree
Hide file tree
Showing 7 changed files with 170 additions and 9 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions leaf/leaf-rpc-proto/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ pub enum ReqKind {
CreateSubspace,
ImportSubspaceSecret(SubspaceSecretKey),
GetSubspaceSecret(SubspaceId),
GetLocalSecret(String),
SetLocalSecret(String, Option<String>),
ListLocalSecrets,
}

#[derive(borsh::BorshSerialize, borsh::BorshDeserialize, Debug)]
Expand All @@ -72,4 +75,7 @@ pub enum RespKind {
CreateSubspace(SubspaceId),
ImportSubspaceSecret(SubspaceId),
GetSubspaceSecret(Option<SubspaceSecretKey>),
GetLocalSecret(Option<String>),
SetLocalSecret,
ListLocalSecrets(HashMap<String, String>),
}
1 change: 1 addition & 0 deletions leaf/leaf-rpc-server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ tokio = { version = "1.37.0", default-features = false, features = ["macros"] }
tower-http = { version = "0.5.2", features = ["trace"] }
tracing = "0.1.40"
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
redb = "2.1.2"

leaf-protocol = { path = "../leaf-protocol", version = "0.0.1" }
leaf-rpc-proto = { path = "../leaf-rpc-proto", version = "0.0.1" }
15 changes: 14 additions & 1 deletion leaf/leaf-rpc-server/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,12 @@ pub static ARGS: Lazy<Args> = Lazy::new(Args::parse);
pub static CLIENT: Lazy<reqwest::Client> =
Lazy::new(|| reqwest::ClientBuilder::new().build().unwrap());

const SECRET_TABLE: redb::TableDefinition<&str, String> = redb::TableDefinition::new("secrets");

pub type AppState = Arc<AppStateInner>;
pub struct AppStateInner {
pub leaf: LeafIroh,
pub secretdb: Arc<redb::Database>,
}

pub type AppResult<T> = Result<T, AppError>;
Expand Down Expand Up @@ -77,6 +80,16 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
let leaf_store = LeafIrohStore::new(node.client().clone());
let leaf = Leaf::new(leaf_store);

let secretdb = Arc::new(redb::Builder::new().create(ARGS.data_dir.join("secrets"))?);
{
let tx = secretdb.begin_write()?;
{
// Make sure that secrets table exists
tx.open_table(SECRET_TABLE)?;
}
tx.commit()?;
}

// Spawn a task to handle the debug CLI commands
let iroh = node.client().clone();
tokio::spawn(handle_cli_prompts(iroh));
Expand All @@ -85,7 +98,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
let router = Router::new()
.route("/", get(proto::ws_handler))
.layer(TraceLayer::new_for_http())
.with_state(Arc::new(AppStateInner { leaf }));
.with_state(Arc::new(AppStateInner { leaf, secretdb }));

let listener = tokio::net::TcpListener::bind(("0.0.0.0", args.port)).await?;
tracing::info!("Starting server on port {}", args.port);
Expand Down
74 changes: 70 additions & 4 deletions leaf/leaf-rpc-server/src/proto.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
use std::collections::HashMap;
use std::{collections::HashMap, sync::Arc};

use axum::{extract::State, response::IntoResponse};
use fastwebsockets::{Frame, OpCode, Payload, WebSocketError};
use futures::StreamExt;
use leaf_protocol::prelude::*;
use leaf_rpc_proto::*;

use crate::{AppState, ARGS};
use crate::{AppState, ARGS, SECRET_TABLE};

pub async fn ws_handler(
state: State<AppState>,
Expand All @@ -28,6 +28,7 @@ async fn handle_client(
fut: fastwebsockets::upgrade::UpgradeFut,
) -> Result<(), WebSocketError> {
let leaf = &state.leaf;
let secretdb = state.secretdb.clone();
let mut ws = fastwebsockets::FragmentCollector::new(fut.await?);
let mut authenticated = false;

Expand All @@ -48,7 +49,7 @@ async fn handle_client(
let req = Req::deserialize(&mut &*frame.payload)?;

if authenticated {
let resp = handle_req(leaf, req).await;
let resp = handle_req(leaf, secretdb.clone(), req).await;
let mut buf = Vec::new();
resp.serialize(&mut buf)?;
ws.write_frame(Frame::binary(Payload::Owned(buf))).await?;
Expand Down Expand Up @@ -103,7 +104,7 @@ async fn handle_client(
Ok(())
}

async fn handle_req(leaf: &LeafIroh, req: Req) -> Resp {
async fn handle_req(leaf: &LeafIroh, secretdb: Arc<redb::Database>, req: Req) -> Resp {
let kind = match req.kind {
ReqKind::Authenticate(_) => {
// TODO: we can hit this somehow when restarting the RPC server while Weird tries to
Expand All @@ -130,6 +131,9 @@ async fn handle_req(leaf: &LeafIroh, req: Req) -> Resp {
ReqKind::CreateSubspace => create_subspace(leaf).await,
ReqKind::ImportSubspaceSecret(secret) => import_subspace_secret(leaf, secret).await,
ReqKind::GetSubspaceSecret(subspace) => get_subspace_secret(leaf, subspace).await,
ReqKind::GetLocalSecret(key) => get_local_secret(secretdb, key).await,
ReqKind::SetLocalSecret(key, value) => set_local_secret(secretdb, key, value).await,
ReqKind::ListLocalSecrets => list_local_secrets(secretdb).await,
};
Resp {
id: req.id,
Expand Down Expand Up @@ -255,3 +259,65 @@ async fn get_subspace_secret(
leaf.get_subspace_secret(subspace).await?,
))
}

async fn get_local_secret(
secretdb: Arc<redb::Database>,
key: String,
) -> std::result::Result<RespKind, anyhow::Error> {
tokio::task::spawn_blocking(move || {
let transaction = secretdb.begin_read()?;
let value = {
let table = transaction.open_table(SECRET_TABLE)?;
let v = table.get(key.as_str())?;
v.map(|guard| guard.value())
};

Ok(RespKind::GetLocalSecret(value))
})
.await
.map_err(|_| anyhow::format_err!("Error executing database operation"))?
}

async fn set_local_secret(
secretdb: Arc<redb::Database>,
key: String,
value: Option<String>,
) -> std::result::Result<RespKind, anyhow::Error> {
tokio::task::spawn_blocking(move || {
let transaction = secretdb.begin_write()?;
{
let mut table = transaction.open_table(SECRET_TABLE)?;
if let Some(value) = value {
table.insert(key.as_str(), value)?;
} else {
table.remove(key.as_str())?;
}
}
transaction.commit()?;

Ok(RespKind::SetLocalSecret)
})
.await
.map_err(|_| anyhow::format_err!("Error executing database operation"))?
}

async fn list_local_secrets(
secretdb: Arc<redb::Database>,
) -> std::result::Result<RespKind, anyhow::Error> {
tokio::task::spawn_blocking(move || {
let mut map = HashMap::default();
let transaction = secretdb.begin_read()?;
{
let table = transaction.open_table(SECRET_TABLE)?;
let records = table.range::<&str>(..)?;
for record in records {
let (key, value) = record?;
map.insert(key.value().into(), value.value());
}
};

Ok(RespKind::ListLocalSecrets(map))
})
.await
.map_err(|_| anyhow::format_err!("Error executing database operation"))?
}
78 changes: 74 additions & 4 deletions leaf/ts/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,10 @@ export type ReqKind =
| { GetNamespaceSecret: NamespaceSecretKey }
| { CreateSubspace: Unit }
| { ImportSubspaceSecret: SubspaceSecretKey }
| { GetSubspaceSecret: SubspaceId };
| { GetSubspaceSecret: SubspaceId }
| { GetLocalSecret: string }
| { SetLocalSecret: { key: string; value?: string } }
| { ListLocalSecrets: Unit };
export const ReqKindSchema = BorshSchema.Enum({
Authenticate: BorshSchema.String,
ReadEntity: ExactLinkSchema,
Expand All @@ -115,7 +118,13 @@ export const ReqKindSchema = BorshSchema.Enum({
GetNamespaceSecret: NamespaceIdSchema,
CreateSubspace: BorshSchema.Unit,
ImportSubspaceSecret: SubspaceSecretKeySchema,
GetSubspaceSecret: SubspaceIdSchema
GetSubspaceSecret: SubspaceIdSchema,
GetLocalSecret: BorshSchema.String,
SetLocalSecret: BorshSchema.Struct({
key: BorshSchema.String,
value: BorshSchema.Option(BorshSchema.String)
}),
ListLocalSecrets: BorshSchema.Unit
});

export type Req = {
Expand Down Expand Up @@ -175,7 +184,10 @@ export type RespKind =
| { GetNamespaceSecret: NamespaceSecretKey | null }
| { CreateSubspace: SubspaceId }
| { ImportSubspaceSecret: SubspaceId }
| { GetSubspaceSecret: SubspaceSecretKey | null };
| { GetSubspaceSecret: SubspaceSecretKey | null }
| { GetLocalSecret: string | null }
| { SetLocalSecret: Unit }
| { ListLocalSecrets: { key: string; value: string }[] };
export const RespKindSchema = BorshSchema.Enum({
Authenticated: BorshSchema.Unit,
ReadEntity: BorshSchema.Option(
Expand All @@ -191,7 +203,12 @@ export const RespKindSchema = BorshSchema.Enum({
GetNamespaceSecret: BorshSchema.Option(NamespaceSecretKeySchema),
CreateSubspace: SubspaceIdSchema,
ImportSubspaceSecret: SubspaceIdSchema,
GetSubspaceSecret: BorshSchema.Option(SubspaceSecretKeySchema)
GetSubspaceSecret: BorshSchema.Option(SubspaceSecretKeySchema),
GetLocalSecret: BorshSchema.Option(BorshSchema.String),
SetLocalSecret: BorshSchema.Unit,
ListLocalSecrets: BorshSchema.Vec(
BorshSchema.Struct({ key: BorshSchema.String, value: BorshSchema.String })
)
});

export type RespResult = { Err: string } | { Ok: RespKind };
Expand Down Expand Up @@ -653,4 +670,57 @@ export class RpcClient {
throw 'Invalid RPC response';
}
}

/**
* Get's the value of a private secret that is stored locally on the Leaf RPC server and not
* shared over the network like the rest of the normal Leaf entity data, which is treated as
* public.
*
* @param key the key of the secret to get.
* @returns the value of the secret if it is present.
*/
async get_local_secret(key: string): Promise<string | null> {
const resp = await this.#send_req({ GetLocalSecret: key });
const respKind = this.#unwrap_resp(resp);
if ('GetLocalSecret' in respKind) {
return respKind.GetLocalSecret;
} else {
throw 'Invalid RPC response';
}
}

/**
* Set's the value of a private secret that is stored locally on the Leaf RPC server and not
* shared over the network like the rest of the normal Leaf entity data, which is treated as
* public.
*
* @param key the key of the secret to get.
* @param value the value to set the secret to, or `undefined` if you want to delete it.
* @returns the value of the secret if it is present.
*/
async set_local_secret(key: string, value?: string): Promise<void> {
const resp = await this.#send_req({ SetLocalSecret: { key, value } });
const respKind = this.#unwrap_resp(resp);
if ('SetLocalSecret' in respKind) {
return;
} else {
throw 'Invalid RPC response';
}
}

/**
* Lists all the keys and values of the private secrets that are stored locally on the Leaf RPC
* server.
*
* @returns the list of secrets.
*/
async list_local_secrets(): Promise<{ key: string; value: string }[]> {
const resp = await this.#send_req({ ListLocalSecrets: {} });
const respKind = this.#unwrap_resp(resp);
if ('ListLocalSecrets' in respKind) {
return respKind.ListLocalSecrets;
} else {
throw 'Invalid RPC response';
}
}
}
4 changes: 4 additions & 0 deletions src/routes/(app)/+layout.server.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import type { LayoutServerLoad } from './$types';
import { getSession } from '$lib/rauthy/server';
import { leafClient } from '$lib/leaf';

// TODO: Move this logic to a "SessionProvider" component.
export const load: LayoutServerLoad = async ({ fetch, request }) => {
await leafClient.set_local_secret('test', undefined)
console.log(await leafClient.list_local_secrets());

return await getSession(fetch, request);
};

0 comments on commit e652393

Please sign in to comment.