Something went wrong. Try again.
Monorepo for Tangled tangled.org
Something went wrong. Try again.
8.8 kB · 251 lines
Rust
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252use super::*;use axum::{body::Body, http::Request};use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD};use k256::ecdsa::{Signature, signature::Signer};use tower::ServiceExt;use wiremock::{ Mock, MockServer, ResponseTemplate, matchers::{body_json, header, method, path},};
const ACTOR: &str = "did:plc:abcdefghijklmnopqrstuvwx";const GATE: &str = "did:web:gate.example";const CREATE: &str = "farm.tranquil.delegation.createAccount";const LIST: &str = "farm.tranquil.delegation.listControlledAccounts";const RESOLVE: &str = "sh.tangled.identity.resolveCommitters";
async fn setup() -> (App, MockServer) { let server = MockServer::start().await; let http = reqwest::Client::new(); let directory = Arc::new( JacquardResolver::new(ReqwestHttp::new(http.clone()), Default::default()).with_plc_source( PlcSource::PlcDirectory { base: Uri::parse(format!("{}/", server.uri()).as_str()) .unwrap() .to_owned(), }, ), ); let signing_key = SigningKey::from_slice(&[1; 32]).unwrap(); let gate = gate::Gate::builder( http.clone(), GATE, signing_key.clone(), server.uri(), "did:web:deliberi.example", ) .build(); let app = App { config: Arc::new(RuntimeConfig { did: GATE.into(), tranquil_url: server.uri(), signing_key, }), http, auth: ServiceAuthConfig::new(Did::new_owned(GATE).unwrap(), directory), gate: Arc::new(gate), }; let mut key = vec![0xe7, 1]; key.extend_from_slice( app.config .signing_key .verifying_key() .to_encoded_point(true) .as_bytes(), ); Mock::given(method("GET")).and(path(format!("/{ACTOR}"))).respond_with(ResponseTemplate::new(200).set_body_json(json!({ "id": ACTOR, "verificationMethod": [{"id": format!("{ACTOR}#atproto"), "type":"Multikey", "controller": ACTOR, "publicKeyMultibase": format!("z{}", bs58::encode(key).into_string())}] }))).mount(&server).await; (app, server)}
fn token(app: &App, aud: &str, lxm: &str, exp: u64, key: Option<&SigningKey>) -> String { let payload = json!({"iss": ACTOR, "aud": aud, "lxm": lxm, "iat": now(), "exp": exp, "jti": format!("test-{lxm}")}); let input = format!( "{}.{}", URL_SAFE_NO_PAD.encode(br#"{"alg":"ES256K","typ":"JWT"}"#), URL_SAFE_NO_PAD.encode(serde_json::to_vec(&payload).unwrap()) ); let signature: Signature = key .unwrap_or(&app.config.signing_key) .sign(input.as_bytes()); format!("{input}.{}", URL_SAFE_NO_PAD.encode(signature.to_bytes()))}
async fn request(app: App, token: Option<&str>) -> Response { let mut request = Request::post("/xrpc/sh.tangled.delegation.createAccount") .header("content-type", "application/json"); if let Some(token) = token { request = request.header("authorization", format!("Bearer {token}")); } router(app) .oneshot( request .body(Body::from(json!({"handle":"org.example"}).to_string())) .unwrap(), ) .await .unwrap()}
async fn email(server: &MockServer, values: Vec<&str>) { Mock::given(method("POST")) .and(path(format!("/xrpc/{RESOLVE}"))) .and(body_json(json!({"actor": ACTOR}))) .respond_with(ResponseTemplate::new(200).set_body_json(json!({"committers": values}))) .mount(server) .await;}
#[tokio::test]async fn rejects_missing_wrong_audience_expired_wrong_method_and_bad_signature() { let (app, server) = setup().await; let bad_key = SigningKey::from_slice(&[2; 32]).unwrap(); let tokens = [ None, Some(token( &app, "did:web:other.example", CREATE, now() + 60, None, )), Some(token(&app, GATE, CREATE, now() - 1, None)), Some(token(&app, GATE, RESOLVE, now() + 60, None)), Some(token(&app, GATE, CREATE, now() + 60, Some(&bad_key))), ]; for token in tokens { assert!( request(app.clone(), token.as_deref()) .await .status() .is_client_error() ); } assert!( server .received_requests() .await .unwrap() .iter() .all(|r| r.method == "GET") );}
#[tokio::test]async fn requires_an_email_not_the_did_fallback_and_rejects_replay() { let (app, server) = setup().await; email(&server, vec![ACTOR]).await; let jwt = token(&app, GATE, CREATE, now() + 60, None); let response = request(app.clone(), Some(&jwt)).await; assert_eq!(response.status(), StatusCode::FORBIDDEN); let body = axum::body::to_bytes(response.into_body(), 4096) .await .unwrap(); assert!( std::str::from_utf8(&body) .unwrap() .contains("VerifiedEmailRequired") ); assert!(request(app, Some(&jwt)).await.status().is_client_error()); assert_eq!( server .received_requests() .await .unwrap() .iter() .filter(|r| r.method == "POST") .count(), 1 );}
#[tokio::test]async fn upstream_client_errors_pass_through_and_server_errors_fail_closed() { let (app, server) = setup().await; let input = || CreateInput { handle: "org.example".into(), }; // No email mock yet, so the resolver 404s and the gate relays that status. let error = app .create(ACTOR, "create-token", input()) .await .err() .unwrap(); assert_eq!(error.status, StatusCode::NOT_FOUND); assert_eq!(error.error, "UpstreamRejected"); email(&server, vec!["person@example.com"]).await; // Tranquil rejecting the creation is relayed with Tranquil's own status, // error code, and message, so a taken handle is distinguishable from the // per-controller cap. Mock::given(path("/xrpc/_delegation.createDelegatedAccount")) .respond_with(ResponseTemplate::new(400).set_body_json( json!({"error": "InvalidDelegation", "message": "delegate cap reached"}), )) .up_to_n_times(1) .mount(&server) .await; let error = app .create(ACTOR, "create-token", input()) .await .err() .unwrap(); assert_eq!(error.status, StatusCode::BAD_REQUEST); assert_eq!(error.error, "InvalidDelegation"); assert_eq!(error.message.as_deref(), Some("delegate cap reached")); Mock::given(path("/xrpc/_delegation.createDelegatedAccount")) .respond_with(ResponseTemplate::new(503)) .mount(&server) .await; let error = app .create(ACTOR, "create-token", input()) .await .err() .unwrap(); assert_eq!(error.status, StatusCode::BAD_GATEWAY); assert_eq!(error.error, "UpstreamUnavailable");}
#[tokio::test]async fn rejects_missing_nonce() { let (app, _) = setup().await; let claims: ServiceAuthClaims = serde_json::from_value(json!({ "iss": ACTOR, "aud": GATE, "iat": now(), "exp": now() + 60, "lxm": CREATE, "jti": null })) .unwrap(); assert!(app.verify_claims(&claims).await.is_err());}
#[tokio::test]async fn forwards_the_create_token_without_controller_overrides() { let (app, server) = setup().await; let create = token(&app, GATE, CREATE, now() + 60, None); email(&server, vec!["person@example.com"]).await; Mock::given(method("POST")) .and(path("/xrpc/_delegation.createDelegatedAccount")) .and(header("authorization", format!("Bearer {create}"))) .and(body_json( json!({"handle":"org.example", "controllerScopes":OWNER}), )) .respond_with( ResponseTemplate::new(200) .set_body_json(json!({"did":"did:plc:org", "handle":"org.example"})), ) .expect(1) .mount(&server) .await; let response = request(app, Some(&create)).await; assert_eq!(response.status(), StatusCode::OK); let body = axum::body::to_bytes(response.into_body(), 4096) .await .unwrap(); let account: Account = serde_json::from_slice(&body).unwrap(); assert_eq!(account.controller_did, ACTOR); // The gate never enumerates delegates or opens a session of its own. assert!(server.received_requests().await.unwrap().iter().all(|r| { r.url.query().is_none() && !r.url.path().contains("createSession") && !r.url.path().contains("listControlledAccounts") }));}