From c3842a6d3b84322abe9b1717e1ad1416f78d2a12 Mon Sep 17 00:00:00 2001 From: oxalica Date: Fri, 18 Oct 2024 11:12:29 -0400 Subject: [PATCH] feat(webapi): impl identity description retrieval --- blah-types/src/server.rs | 10 ++++++++ blahd/Cargo.toml | 2 +- blahd/src/database.rs | 23 ++++++++++++++++++ blahd/src/lib.rs | 20 ++++++++++++++-- blahd/tests/webapi.rs | 52 +++++++++++++++++++++++++++++++++++++--- docs/webapi.yaml | 25 +++++++++++++++++++ 6 files changed, 126 insertions(+), 6 deletions(-) diff --git a/blah-types/src/server.rs b/blah-types/src/server.rs index f0d9325..fce6c8a 100644 --- a/blah-types/src/server.rs +++ b/blah-types/src/server.rs @@ -5,6 +5,7 @@ use std::fmt; use serde::{Deserialize, Serialize}; use url::Url; +use crate::identity::UserIdentityDesc; use crate::msg::{Id, MemberPermission, RoomAttrs, SignedChatMsgWithId}; use crate::PubKey; @@ -172,6 +173,15 @@ pub struct RoomMember { pub last_seen_cid: Option, } +/// Server cached user identity description. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +pub struct UserIdentityDescResponse { + /// The identity description of the requested user. + #[cfg_attr(feature = "schemars", schemars(with = "UserIdentityDesc"))] + pub identity: I, +} + /// A server-to-client event. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] diff --git a/blahd/Cargo.toml b/blahd/Cargo.toml index 038a7da..db5c340 100644 --- a/blahd/Cargo.toml +++ b/blahd/Cargo.toml @@ -30,7 +30,7 @@ serde = { version = "1", features = ["derive"] } serde-constant = "0.1" serde-inline-default = "0.2" serde_jcs = "0.1" -serde_json = "1" +serde_json = { version = "1", features = ["raw_value"] } serde_urlencoded = "0.7" sha2 = "0.10" tokio = { version = "1", features = ["macros", "rt-multi-thread", "signal", "sync", "time"] } diff --git a/blahd/src/database.rs b/blahd/src/database.rs index a4bc995..03cede1 100644 --- a/blahd/src/database.rs +++ b/blahd/src/database.rs @@ -10,8 +10,10 @@ use blah_types::msg::{ use blah_types::server::RoomMetadata; use blah_types::{Id, PubKey, Signee, UserKey}; use parking_lot::Mutex; +use rusqlite::types::FromSqlError; use rusqlite::{named_params, params, prepare_cached_and_bind, Connection, OpenFlags, Row}; use serde::Deserialize; +use serde_json::value::RawValue as JsonRawValue; use crate::middleware::ApiError; @@ -170,6 +172,27 @@ fn parse_room_metadata(row: &Row<'_>) -> Result { pub trait TransactionOps { fn conn(&self) -> &Connection; + fn get_user_id_desc_by_uid(&self, uid: i64) -> Result> { + prepare_cached_and_bind!( + self.conn(), + r" + SELECT `id_desc` + FROM `user` + WHERE `uid` = :uid + " + ) + .raw_query() + .and_then(|row| { + let json = JsonRawValue::from_string(row.get(0)?).map_err(|err| { + FromSqlError::Other(format!("invalid id_desc in database: {err}").into()) + })?; + Ok::<_, rusqlite::Error>(json) + }) + .next() + .ok_or(ApiError::UserNotFound)? + .map_err(Into::into) + } + fn get_user(&self, UserKey { id_key, act_key }: &UserKey) -> Result<(i64, ServerPermission)> { prepare_cached_and_bind!( self.conn(), diff --git a/blahd/src/lib.rs b/blahd/src/lib.rs index 3cd0039..b62ca12 100644 --- a/blahd/src/lib.rs +++ b/blahd/src/lib.rs @@ -18,7 +18,7 @@ use blah_types::msg::{ }; use blah_types::server::{ ErrorResponseWithChallenge, RoomList, RoomMember, RoomMemberList, RoomMetadata, RoomMsgs, - ServerCapabilities, ServerMetadata, + ServerCapabilities, ServerMetadata, UserIdentityDescResponse, }; use blah_types::{get_timestamp, Id, PubKey, Signed, UserKey}; use data_encoding::BASE64_NOPAD; @@ -29,6 +29,7 @@ use parking_lot::Mutex; use serde::de::DeserializeOwned; use serde::{Deserialize, Deserializer, Serialize}; use serde_inline_default::serde_inline_default; +use serde_json::value::RawValue as JsonRawValue; use sha2::Digest; use url::Url; use utils::ExpiringSet; @@ -172,7 +173,8 @@ pub fn router(st: Arc) -> Router { // TODO!: remove this. .route("/room/:rid/admin", r().post(post_room_admin)) .route("/room/:rid/member", r().get(list_room_member).post(post_room_member)) - .route("/room/:rid/member/:uid", r().get(get_room_member).delete(delete_room_member).patch(patch_room_member)) + .route("/room/:rid/member/:idkey", r().get(get_room_member).delete(delete_room_member).patch(patch_room_member)) + .route("/room/:rid/member/:idkey/identity", r().get(get_room_member_identity)) .fallback(fallback_route) ; @@ -620,6 +622,20 @@ async fn patch_room_member( }) } +async fn get_room_member_identity( + st: ArcState, + R(Path((rid, id_key)), _): RE>, + Auth(user): Auth, +) -> Result>>, ApiError> { + st.db.with_read(|txn| { + // Check membership. + let _ = txn.get_room_member(rid, &user)?; + let (uid, ..) = txn.get_room_member_by_id_key(rid, &id_key)?; + let identity = txn.get_user_id_desc_by_uid(uid)?; + Ok(Json(UserIdentityDescResponse { identity })) + }) +} + async fn delete_room( st: ArcState, R(Path(rid), _): RE>, diff --git a/blahd/tests/webapi.rs b/blahd/tests/webapi.rs index 64b67fc..ac2a37d 100644 --- a/blahd/tests/webapi.rs +++ b/blahd/tests/webapi.rs @@ -17,7 +17,7 @@ use blah_types::msg::{ }; use blah_types::server::{ RoomList, RoomMember, RoomMemberList, RoomMetadata, RoomMsgs, ServerEvent, ServerMetadata, - UserRegisterChallenge, + UserIdentityDescResponse, UserRegisterChallenge, }; use blah_types::{Id, SignExt, Signed, UserKey}; use blahd::{AppState, Database}; @@ -31,6 +31,7 @@ use rstest::{fixture, rstest}; use rusqlite::{params, Connection}; use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; +use serde_json::json; use sha2::{Digest, Sha256}; use tokio::net::TcpListener; @@ -68,6 +69,7 @@ nonce_rotate_secs = 60 }; struct User { + name: u8, pubkeys: UserKey, id_priv: SigningKey, act_priv: SigningKey, @@ -79,6 +81,7 @@ impl User { let id_priv = SigningKey::from_bytes(&[b; 32]); let act_priv = SigningKey::from_bytes(&[b.to_ascii_lowercase(); 32]); Self { + name: b, pubkeys: UserKey { id_key: id_priv.verifying_key().into(), act_key: act_priv.verifying_key().into(), @@ -433,7 +436,7 @@ fn server() -> Server { .prepare( r" INSERT INTO `user` (`id_key`, `permission`, `last_fetch_time`, `id_desc`) - VALUES (?, ?, 0, '{}') + VALUES (?, ?, 0, ?) ", ) .unwrap(); @@ -449,8 +452,10 @@ fn server() -> Server { (&*ALICE, ServerPermission::ALL), (&BOB, ServerPermission::empty()), ] { + // Fake value. + let id_desc = json!({"user": user.name as char }).to_string(); add_user - .execute(params![user.pubkeys.id_key, perm]) + .execute(params![user.pubkeys.id_key, perm, id_desc]) .unwrap(); let uid = conn.last_insert_rowid(); add_act_key @@ -1793,3 +1798,44 @@ async fn room_mgmt_perm(server: Server) { // Bob can chat again. server.post_chat(rid, &BOB, "yay").await.unwrap(); } + +#[rstest] +#[tokio::test] +async fn get_member_id_desc(server: Server) { + let rid = server + .create_room(&ALICE, RoomAttrs::PUBLIC_JOINABLE, "public") + .await + .unwrap(); + + let get_id_desc = |src_user: &User, tgt_user: &User| { + server + .get::>>( + &format!("/room/{rid}/member/{}/identity", tgt_user.pubkeys.id_key), + Some(&auth(src_user)), + ) + .map_ok(|desc| desc.identity["user"].as_str().unwrap().to_owned()) + }; + + // Current user not in the room. + get_id_desc(&BOB, &ALICE) + .await + .expect_api_err(StatusCode::NOT_FOUND, "room_not_found"); + + // Target user not in the room. + get_id_desc(&ALICE, &BOB) + .await + .expect_api_err(StatusCode::NOT_FOUND, "member_not_found"); + + // OK, get self. + let desc = get_id_desc(&ALICE, &ALICE).await.unwrap(); + assert_eq!(desc, "A"); + + server + .join_room(rid, &BOB, MemberPermission::MAX_SELF_ADD) + .await + .unwrap(); + + // Ok, get member. + let desc = get_id_desc(&ALICE, &BOB).await.unwrap(); + assert_eq!(desc, "B"); +} diff --git a/docs/webapi.yaml b/docs/webapi.yaml index 38430c1..27399c4 100644 --- a/docs/webapi.yaml +++ b/docs/webapi.yaml @@ -688,6 +688,31 @@ paths: schema: $ref: '#/components/schemas/ApiError' + /_blah/room/{rid}/member/{member_id_key}/identity: + get: + summary: Get identity description of a room member + + parameters: + - name: Authorization + in: header + description: User authentication token. + schema: + $ref: '#/components/schemas/Signed-Auth' + + responses: + 200: + content: + application/json: + schema: + $ref: '#/components/schemas/UserIdentityDescResponse' + + 404: + description: | + Room does not exist, or either user is not a room member. + content: + application/json: + schema: + $ref: '#/components/schemas/ApiError' # Ideally we should generate these from src, but we need to # WAIT: https://github.com/juhaku/utoipa/pull/1034