Skip to content

Commit

Permalink
no-database endpoints
Browse files Browse the repository at this point in the history
  • Loading branch information
TheButlah committed Aug 5, 2024
1 parent ed47264 commit 95bc5d9
Show file tree
Hide file tree
Showing 6 changed files with 275 additions and 11 deletions.
43 changes: 43 additions & 0 deletions Cargo.lock

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

4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -49,15 +49,19 @@ bevy_web_asset = { git = "https://github.com/Schmarni-Dev/bevy_web_asset", rev =
bytes = "1.5.0"
clap = { version = "4.4.11", features = ["derive"] }
color-eyre = "0.6"
did-simple.path = "crates/did-simple"
egui = "0.26"
egui-picking = { path = "crates/egui-picking" }
eyre = "0.6"
futures = "0.3.30"
hex-literal = "0.4.1"
jose-jwk = { version = "0.1.2", default-features = false }
lightyear = "0.12"
openxr = "0.18"
picking-xr = { path = "crates/picking-xr" }
pin-project = "1"
rand = "0.8.5"
rand_chacha = "0.3.1"
rand_xoshiro = "0.6.0"
random-number = "0.1.8"
replicate-client.path = "crates/replicate/client"
Expand Down
11 changes: 10 additions & 1 deletion apps/identity_server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,17 @@ publish = false
axum.workspace = true
clap.workspace = true
color-eyre.workspace = true
did-simple.workspace = true
jose-jwk = { workspace = true, default-features = false }
rand.workspace = true
serde.workspace = true
serde_json.workspace = true
tokio = { workspace = true, features = ["full"] }
tower-http = { workspace = true, features = ["trace"] }
tracing-subscriber = { workspace = true, features = ["env-filter"] }
tracing.workspace = true
serde.workspace = true
uuid = { workspace = true, features = ["std", "v4", "serde"] }

[dev-dependencies]
base64.workspace = true
hex-literal.workspace = true
7 changes: 6 additions & 1 deletion apps/identity_server/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
mod uuid;
pub mod v1;

use axum::routing::get;
use tower_http::trace::TraceLayer;

/// Main router of API
pub fn router() -> axum::Router<()> {
let v1_router = crate::v1::RouterConfig {
..Default::default()
}
.build();
axum::Router::new()
.route("/", get(root))
.nest("/api/v1", crate::v1::router())
.nest("/api/v1", v1_router)
.layer(TraceLayer::new_for_http())
}

Expand Down
104 changes: 104 additions & 0 deletions apps/identity_server/src/uuid.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
//! Mockable UUID generation.

use ::uuid::Uuid;
use std::sync::atomic::{AtomicUsize, Ordering};

/// Handles generation of UUIDs. This is used instead of the uuid crate directly,
/// to better support deterministic UUID creation in tests.
#[derive(Debug)]
pub struct UuidProvider {
#[cfg(not(test))]
provider: ThreadLocalRng,
#[cfg(test)]
provider: Box<dyn UuidProviderT>,
}

impl UuidProvider {
#[allow(dead_code)]
pub fn new_thread_local() -> Self {
Self {
#[cfg(test)]
provider: Box::new(ThreadLocalRng),
#[cfg(not(test))]
provider: ThreadLocalRng,
}
}

/// Allows controlling the sequence of generated UUIDs. Only available in
/// `cfg(test)`.
#[allow(dead_code)]
#[cfg(test)]
pub fn new_from_sequence(uuids: Vec<Uuid>) -> Self {
Self {
provider: Box::new(TestSequence::new(uuids)),
}
}

#[inline]
pub fn next_v4(&self) -> Uuid {
self.provider.next_v4()
}
}

impl Default for UuidProvider {
fn default() -> Self {
Self::new_thread_local()
}
}

trait UuidProviderT: std::fmt::Debug + Send + Sync + 'static {
fn next_v4(&self) -> Uuid;
}

#[derive(Debug)]
struct ThreadLocalRng;
impl UuidProviderT for ThreadLocalRng {
fn next_v4(&self) -> Uuid {
Uuid::new_v4()
}
}

/// Provides UUIDs from a known sequence. Useful for tests.
#[derive(Debug)]
struct TestSequence {
uuids: Vec<Uuid>,
pos: AtomicUsize,
}
impl TestSequence {
/// # Panics
/// Panics if len of vec is 0
#[allow(dead_code)]
fn new(uuids: Vec<Uuid>) -> Self {
assert!(!uuids.is_empty());
Self {
uuids,
pos: AtomicUsize::new(0),
}
}
}

impl UuidProviderT for TestSequence {
fn next_v4(&self) -> Uuid {
let curr_pos = self.pos.fetch_add(1, Ordering::SeqCst) % self.uuids.len();
self.uuids[curr_pos]
}
}

fn _assert_bounds(p: UuidProvider) {
fn helper(_p: impl std::fmt::Debug + Send + Sync + 'static) {}
helper(p)
}

#[cfg(test)]
mod test {
use super::*;

#[test]
fn test_sequence_order() {
let uuids: Vec<Uuid> = (0..4).map(|_| Uuid::new_v4()).collect();
let sequence = TestSequence::new(uuids.clone());
for uuid in uuids {
assert_eq!(uuid, sequence.next_v4());
}
}
}
117 changes: 108 additions & 9 deletions apps/identity_server/src/v1/mod.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,116 @@
//! V1 of the API. This is subject to change until we commit to stability, after
//! which point any breaking changes will go in a V2 api.

use axum::{routing::post, Json, Router};
use serde::{Deserialize, Serialize};
use std::{collections::BTreeSet, sync::Arc};

/// Router of API V1
pub fn router() -> Router {
Router::new().route("/create", post(create))
use axum::{
extract::{Path, State},
response::Redirect,
routing::{get, post},
Json, Router,
};
use did_simple::crypto::ed25519;
use jose_jwk::Jwk;
use uuid::Uuid;

use crate::uuid::UuidProvider;

#[derive(Debug)]
struct RouterState {
uuid_provider: UuidProvider,
}
type SharedState = Arc<RouterState>;

/// Configuration for the V1 api's router.
#[derive(Debug, Default)]
pub struct RouterConfig {
pub uuid_provider: UuidProvider,
}

impl RouterConfig {
pub fn build(self) -> Router {
Router::new()
.route("/create", post(create))
.route("/users/:id/did.json", get(read))
.with_state(Arc::new(RouterState {
uuid_provider: self.uuid_provider,
}))
}
}

async fn create(state: State<SharedState>, _pubkey: Json<Jwk>) -> Redirect {
let uuid = state.uuid_provider.next_v4();
Redirect::to(&format!("/users/{}/did.json", uuid.as_hyphenated()))
}

async fn create(_pubkey: Json<JWK>) -> String {
String::from("did:web:todo")
async fn read(_state: State<SharedState>, Path(_user_id): Path<Uuid>) -> Json<Jwk> {
Json(ed25519_pub_jwk(
ed25519::SigningKey::random().verifying_key(),
))
}

#[derive(Debug, Serialize, Deserialize)]
struct JWK;
fn ed25519_pub_jwk(pub_key: ed25519::VerifyingKey) -> jose_jwk::Jwk {
Jwk {
key: jose_jwk::Okp {
crv: jose_jwk::OkpCurves::Ed25519,
x: pub_key.into_inner().as_bytes().as_slice().to_owned().into(),
d: None,
}
.into(),
prm: jose_jwk::Parameters {
ops: Some(BTreeSet::from([jose_jwk::Operations::Verify])),
..Default::default()
},
}
}

#[cfg(test)]
mod test {
use base64::Engine as _;

use super::*;

#[test]
fn pub_jwk_test_vectors() {
// See https://datatracker.ietf.org/doc/html/rfc8037#appendix-A.2
let rfc_example = serde_json::json! ({
"kty": "OKP",
"crv": "Ed25519",
"x": "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo"
});
let pubkey_bytes = hex_literal::hex!(
"d7 5a 98 01 82 b1 0a b7 d5 4b fe d3 c9 64 07 3a
0e e1 72 f3 da a6 23 25 af 02 1a 68 f7 07 51 1a"
);
assert_eq!(
base64::prelude::BASE64_URL_SAFE_NO_PAD
.decode(rfc_example["x"].as_str().unwrap())
.unwrap(),
pubkey_bytes,
"sanity check: example bytes should match, they come from the RFC itself"
);

let input_key = ed25519::VerifyingKey::try_from_bytes(&pubkey_bytes).unwrap();
let mut output_jwk = ed25519_pub_jwk(input_key);

// Check all additional outputs for expected values
assert_eq!(
output_jwk.prm.ops.take().unwrap(),
BTreeSet::from([jose_jwk::Operations::Verify]),
"expected Verify as a supported operation"
);
let output_jwk = output_jwk; // Freeze mutation from here on out

// Check serialization and deserialization against the rfc example
assert_eq!(
serde_json::from_value::<Jwk>(rfc_example.clone()).unwrap(),
output_jwk,
"deserializing json to Jwk did not match"
);
assert_eq!(
rfc_example,
serde_json::to_value(output_jwk).unwrap(),
"serializing Jwk to json did not match"
);
}
}

0 comments on commit 95bc5d9

Please sign in to comment.