Something went wrong. Try again.
Monorepo for Tangled tangled.org
Something went wrong. Try again.
12 kB · 369 lines
Rust
at sl/gitmirror
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370use 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, query_param},};
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) { setup_with_peers(&[]).await}
async fn setup_with_peers(peers: &[&MockServer]) -> (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, handle_domain: "example".into(), handle_hosts: std::iter::once(server.uri()) .chain(peers.iter().map(|peer| peer.uri())) .collect(), reserved_handles: vec!["gate.example".into()], }), 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 describe_repo(server: &MockServer, status: u16, body: Value) { Mock::given(method("GET")) .and(path("/xrpc/com.atproto.repo.describeRepo")) .and(query_param("repo", "org.example")) .respond_with(ResponseTemplate::new(status).set_body_json(body)) .mount(server) .await;}
async fn handle_free(server: &MockServer) { describe_repo(server, 400, json!({"error": "RepoNotFound"})).await;}
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; handle_free(&server).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; handle_free(&server).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().ends_with("describeRepo")) && !r.url.path().contains("createSession") && !r.url.path().contains("listControlledAccounts") }));}
#[test]fn qualifies_only_single_labels_under_the_handle_domain() { assert_eq!( qualify("Acme.Example", "example").as_deref(), Some("acme.example") ); assert_eq!( qualify("a-1.example", "example").as_deref(), Some("a-1.example") ); for handle in [ "example", ".example", "acme.other", "acme.notexample", "a.b.example", "-acme.example", "acme-.example", "ac_me.example", "acme.example.evil", ] { assert_eq!(qualify(handle, "example"), None, "{handle}"); }}
#[tokio::test]async fn refuses_handles_outside_the_domain_or_reserved_before_checking_email() { let (app, server) = setup().await; for (handle, code) in [ ("org.elsewhere", "InvalidHandle"), ("gate.example", "HandleNotAvailable"), ] { let error = app .create( ACTOR, "create-token", CreateInput { handle: handle.into(), }, ) .await .err() .unwrap(); assert_eq!(error.error, code, "{handle}"); } assert!(server.received_requests().await.unwrap().is_empty());}
#[tokio::test]async fn refuses_a_handle_any_pds_in_the_domain_already_holds() { let input = || CreateInput { handle: "org.example".into(), }; for (status, body) in [ ( 200, json!({"did": "did:plc:someone", "handle": "org.example"}), ), (400, json!({"error": "RepoDeactivated"})), ] { let peer = MockServer::start().await; describe_repo(&peer, status, body).await; let (app, server) = setup_with_peers(&[&peer]).await; email(&server, vec!["person@example.com"]).await; handle_free(&server).await; let error = app .create(ACTOR, "create-token", input()) .await .err() .unwrap(); assert_eq!(error.error, "HandleNotAvailable"); assert!( server .received_requests() .await .unwrap() .iter() .all(|r| !r.url.path().contains("createDelegatedAccount")) ); }
let peer = MockServer::start().await; describe_repo(&peer, 503, json!({})).await; let (app, server) = setup_with_peers(&[&peer]).await; email(&server, vec!["person@example.com"]).await; handle_free(&server).await; let error = app .create(ACTOR, "create-token", input()) .await .err() .unwrap(); assert_eq!(error.error, "UpstreamUnavailable");}