From 8378c4d230b0c2d080363410abd508478c3321b7 Mon Sep 17 00:00:00 2001 From: oxalica Date: Sat, 12 Oct 2024 13:49:11 -0400 Subject: [PATCH] feat(webapi): impl member permission update --- blah-types/src/msg.rs | 11 +++++- blahd/src/database.rs | 16 ++++++++ blahd/src/lib.rs | 37 +++++++++++++++++- blahd/tests/webapi.rs | 90 +++++++++++++++++++++++++++++++++++++++++-- docs/webapi.yaml | 65 ++++++++++++++++++++++++++++++- 5 files changed, 212 insertions(+), 7 deletions(-) diff --git a/blah-types/src/msg.rs b/blah-types/src/msg.rs index 61cabf4..c4bb1bd 100644 --- a/blah-types/src/msg.rs +++ b/blah-types/src/msg.rs @@ -364,12 +364,20 @@ pub struct AddMemberPayload { pub member: RoomMember, } +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(tag = "typ", rename_all = "snake_case", rename = "update_member")] +pub struct UpdateMemberPayload { + pub room: Id, + #[serde(flatten)] + pub member: RoomMember, +} + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(untagged)] pub enum RoomAdminOp { AddMember(AddMemberPayload), RemoveMember(RemoveMemberPayload), - // TODO: RU + // TODO: R } bitflags::bitflags! { @@ -394,6 +402,7 @@ bitflags::bitflags! { // TODO: Should we have multiple levels of removal permission, so that admins // may not remove all other admins? const REMOVE_MEMBER = 1 << 4; + const UPDATE_MEMBER = 1 << 5; const MAX_SELF_ADD = Self::POST_CHAT.bits(); const MAX_PEER_CHAT = Self::POST_CHAT.bits() | Self::DELETE_ROOM.bits() | Self::LIST_MEMBERS.bits(); diff --git a/blahd/src/database.rs b/blahd/src/database.rs index 6829e69..a4bc995 100644 --- a/blahd/src/database.rs +++ b/blahd/src/database.rs @@ -547,6 +547,22 @@ pub trait TransactionOps { Ok(()) } + fn update_room_member(&self, rid: Id, uid: i64, perm: MemberPermission) -> Result<()> { + let updated = prepare_cached_and_bind!( + self.conn(), + r" + UPDATE `room_member` SET + `permission` = :perm + WHERE (`rid`, `uid`) = (:rid, :uid) + " + ) + .raw_execute()?; + if updated != 1 { + return Err(ApiError::UserNotFound); + } + Ok(()) + } + fn remove_room_member(&self, rid: Id, uid: i64) -> Result<()> { // TODO: Check if it is the last member? let updated = prepare_cached_and_bind!( diff --git a/blahd/src/lib.rs b/blahd/src/lib.rs index 6838499..7302c69 100644 --- a/blahd/src/lib.rs +++ b/blahd/src/lib.rs @@ -13,7 +13,7 @@ use axum_extra::extract::WithRejection as R; use blah_types::msg::{ AddMemberPayload, ChatPayload, CreateGroup, CreatePeerChat, CreateRoomPayload, DeleteRoomPayload, MemberPermission, RemoveMemberPayload, RoomAdminOp, RoomAdminPayload, - RoomAttrs, ServerPermission, SignedChatMsgWithId, WithMsgId, + RoomAttrs, ServerPermission, SignedChatMsgWithId, UpdateMemberPayload, WithMsgId, }; use blah_types::server::{ ErrorResponseWithChallenge, RoomList, RoomMember, RoomMemberList, RoomMetadata, RoomMsgs, @@ -171,7 +171,7 @@ pub fn router(st: Arc) -> Router { // TODO!: remove this. .route("/room/:rid/admin", post(post_room_admin)) .route("/room/:rid/member", get(list_room_member).post(post_room_member)) - .route("/room/:rid/member/:uid", delete(delete_room_member)) + .route("/room/:rid/member/:uid", delete(delete_room_member).patch(patch_room_member)) ; let router = router @@ -560,6 +560,39 @@ async fn delete_room_member( }) } +async fn patch_room_member( + st: ArcState, + R(Path((rid, id_key)), _): RE>, + SignedJson(op): SignedJson, +) -> Result { + api_ensure!(rid == op.signee.payload.room, "room id mismatch with URI"); + api_ensure!(!rid.is_peer_chat(), "cannot operate on a peer chat room"); + let op_member = op.signee.payload.member; + api_ensure!(id_key == op_member.user, "user id mismatch with URI"); + + st.db.with_write(|txn| { + let (_src_uid, src_perm, ..) = txn.get_room_member(rid, &op.signee.user)?; + api_ensure!( + src_perm.contains(MemberPermission::UPDATE_MEMBER), + ApiError::PermissionDenied("the user does not have permission to update permissions") + ); + api_ensure!( + src_perm.contains(op_member.permission), + ApiError::PermissionDenied("cannot set a permission higher than setter's") + ); + let (tgt_uid, tgt_prev_perm, ..) = txn.get_room_member_by_id_key(rid, &op_member.user)?; + api_ensure!( + src_perm.contains(tgt_prev_perm), + ApiError::PermissionDenied( + "cannot update a member having higher permission than setter's" + ), + ); + // Checked to exist. + txn.update_room_member(rid, tgt_uid, op_member.permission)?; + Ok(NoContent) + }) +} + async fn delete_room( st: ArcState, R(Path(rid), _): RE>, diff --git a/blahd/tests/webapi.rs b/blahd/tests/webapi.rs index aae26df..415c659 100644 --- a/blahd/tests/webapi.rs +++ b/blahd/tests/webapi.rs @@ -12,8 +12,8 @@ use blah_types::identity::{IdUrl, UserActKeyDesc, UserIdentityDesc, UserProfile} use blah_types::msg::{ self, AddMemberPayload, AuthPayload, ChatPayload, CreateGroup, CreatePeerChat, CreateRoomPayload, DeleteRoomPayload, MemberPermission, RemoveMemberPayload, RichText, - RoomAttrs, ServerPermission, SignedChatMsg, SignedChatMsgWithId, UserRegisterChallengeResponse, - UserRegisterPayload, WithMsgId, + RoomAttrs, ServerPermission, SignedChatMsg, SignedChatMsgWithId, UpdateMemberPayload, + UserRegisterChallengeResponse, UserRegisterPayload, WithMsgId, }; use blah_types::server::{ RoomList, RoomMember, RoomMemberList, RoomMetadata, RoomMsgs, ServerEvent, ServerMetadata, @@ -346,6 +346,33 @@ impl Server { .map_ok(|None| {}) } + fn update_member_perm( + &self, + rid: Id, + act_user: &User, + tgt_user: &User, + permission: MemberPermission, + ) -> impl Future> + use<'_> { + let tgt_user_id = tgt_user.pubkeys.id_key.clone(); + let req = self.sign( + act_user, + UpdateMemberPayload { + room: rid, + member: msg::RoomMember { + permission, + user: tgt_user_id.clone(), + }, + }, + ); + self.request::<_, NoContent>( + Method::PATCH, + &format!("/room/{rid}/member/{tgt_user_id}"), + None, + Some(req), + ) + .map_ok(|None| {}) + } + fn post_chat( &self, rid: Id, @@ -1639,7 +1666,7 @@ async fn room_member(server: Server) { #[rstest] #[tokio::test] -async fn room_management(server: Server) { +async fn room_mgmt_remove(server: Server) { let rid = server .create_room(&ALICE, RoomAttrs::PUBLIC_JOINABLE, "public") .await @@ -1681,3 +1708,60 @@ async fn room_management(server: Server) { .await .expect_api_err(StatusCode::NOT_FOUND, "room_not_found"); } + +#[rstest] +#[tokio::test] +async fn room_mgmt_update_perm(server: Server) { + let rid = server + .create_room(&ALICE, RoomAttrs::PUBLIC_JOINABLE, "public") + .await + .unwrap(); + server + .join_room(rid, &BOB, MemberPermission::MAX_SELF_ADD) + .await + .unwrap(); + + // OK, Alice grants Bob permission to change permission. + server + .update_member_perm( + rid, + &ALICE, + &BOB, + MemberPermission::POST_CHAT | MemberPermission::UPDATE_MEMBER, + ) + .await + .unwrap(); + + // Cannot restrict a member with higher permission. + server + .update_member_perm(rid, &BOB, &ALICE, MemberPermission::empty()) + .await + .expect_api_err(StatusCode::FORBIDDEN, "permission_denied"); + + // OK, Bob restrict themself. + server + .update_member_perm(rid, &BOB, &BOB, MemberPermission::empty()) + .await + .unwrap(); + + // Cannot self-grant permission. + server + .update_member_perm(rid, &BOB, &BOB, MemberPermission::POST_CHAT) + .await + .expect_api_err(StatusCode::FORBIDDEN, "permission_denied"); + + // Bob cannot chat now. + server + .post_chat(rid, &BOB, "no") + .await + .expect_api_err(StatusCode::FORBIDDEN, "permission_denied"); + + // OK, Alice grants Bob permission. + server + .update_member_perm(rid, &ALICE, &BOB, MemberPermission::POST_CHAT) + .await + .unwrap(); + + // Bob can chat again. + server.post_chat(rid, &BOB, "yay").await.unwrap(); +} diff --git a/docs/webapi.yaml b/docs/webapi.yaml index 6d25cd7..2b0137e 100644 --- a/docs/webapi.yaml +++ b/docs/webapi.yaml @@ -594,7 +594,38 @@ paths: schema: $ref: '#/components/schemas/ApiError' - /_blah/room/{rid}/member/{target_id_key}: + /_blah/room/{rid}/member/{member_id_key}: + patch: + summary: Update permission of a room member + + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Signed-UpdateMember' + + responses: + 204: + description: Operation completed. + + 403: + description: | + The user does not have permission to update permission of the + given user. + content: + application/json: + schema: + $ref: '#/components/schemas/ApiError' + + 404: + description: | + Room does not exist, the user does not have permission for the + operation, or the operand user is not a room member. + content: + application/json: + schema: + $ref: '#/components/schemas/ApiError' + delete: summary: Remove a room member. @@ -876,6 +907,38 @@ components: user: type: string + Signed-UpdateMember: + type: object + properties: + sig: + type: string + signee: + type: object + properties: + nonce: + type: integer + format: uint32 + timestamp: + type: integer + format: uint64 + id_key: + type: string + act_key: + type: string + payload: + type: object + properties: + typ: + type: string + const: 'update_member' + room: + type: string + permission: + type: integer + format: int32 + user: + type: string + Signed-RemoveMember: type: object properties: