feat(webapi): impl member permission update

This commit is contained in:
oxalica 2024-10-12 13:49:11 -04:00
parent ad4a38cf43
commit 8378c4d230
5 changed files with 212 additions and 7 deletions

View file

@ -364,12 +364,20 @@ pub struct AddMemberPayload {
pub member: RoomMember, 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)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(untagged)] #[serde(untagged)]
pub enum RoomAdminOp { pub enum RoomAdminOp {
AddMember(AddMemberPayload), AddMember(AddMemberPayload),
RemoveMember(RemoveMemberPayload), RemoveMember(RemoveMemberPayload),
// TODO: RU // TODO: R
} }
bitflags::bitflags! { bitflags::bitflags! {
@ -394,6 +402,7 @@ bitflags::bitflags! {
// TODO: Should we have multiple levels of removal permission, so that admins // TODO: Should we have multiple levels of removal permission, so that admins
// may not remove all other admins? // may not remove all other admins?
const REMOVE_MEMBER = 1 << 4; const REMOVE_MEMBER = 1 << 4;
const UPDATE_MEMBER = 1 << 5;
const MAX_SELF_ADD = Self::POST_CHAT.bits(); const MAX_SELF_ADD = Self::POST_CHAT.bits();
const MAX_PEER_CHAT = Self::POST_CHAT.bits() | Self::DELETE_ROOM.bits() | Self::LIST_MEMBERS.bits(); const MAX_PEER_CHAT = Self::POST_CHAT.bits() | Self::DELETE_ROOM.bits() | Self::LIST_MEMBERS.bits();

View file

@ -547,6 +547,22 @@ pub trait TransactionOps {
Ok(()) 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<()> { fn remove_room_member(&self, rid: Id, uid: i64) -> Result<()> {
// TODO: Check if it is the last member? // TODO: Check if it is the last member?
let updated = prepare_cached_and_bind!( let updated = prepare_cached_and_bind!(

View file

@ -13,7 +13,7 @@ use axum_extra::extract::WithRejection as R;
use blah_types::msg::{ use blah_types::msg::{
AddMemberPayload, ChatPayload, CreateGroup, CreatePeerChat, CreateRoomPayload, AddMemberPayload, ChatPayload, CreateGroup, CreatePeerChat, CreateRoomPayload,
DeleteRoomPayload, MemberPermission, RemoveMemberPayload, RoomAdminOp, RoomAdminPayload, DeleteRoomPayload, MemberPermission, RemoveMemberPayload, RoomAdminOp, RoomAdminPayload,
RoomAttrs, ServerPermission, SignedChatMsgWithId, WithMsgId, RoomAttrs, ServerPermission, SignedChatMsgWithId, UpdateMemberPayload, WithMsgId,
}; };
use blah_types::server::{ use blah_types::server::{
ErrorResponseWithChallenge, RoomList, RoomMember, RoomMemberList, RoomMetadata, RoomMsgs, ErrorResponseWithChallenge, RoomList, RoomMember, RoomMemberList, RoomMetadata, RoomMsgs,
@ -171,7 +171,7 @@ pub fn router(st: Arc<AppState>) -> Router {
// TODO!: remove this. // TODO!: remove this.
.route("/room/:rid/admin", post(post_room_admin)) .route("/room/:rid/admin", post(post_room_admin))
.route("/room/:rid/member", get(list_room_member).post(post_room_member)) .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 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<Path<(Id, PubKey)>>,
SignedJson(op): SignedJson<UpdateMemberPayload>,
) -> Result<NoContent, ApiError> {
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( async fn delete_room(
st: ArcState, st: ArcState,
R(Path(rid), _): RE<Path<Id>>, R(Path(rid), _): RE<Path<Id>>,

View file

@ -12,8 +12,8 @@ use blah_types::identity::{IdUrl, UserActKeyDesc, UserIdentityDesc, UserProfile}
use blah_types::msg::{ use blah_types::msg::{
self, AddMemberPayload, AuthPayload, ChatPayload, CreateGroup, CreatePeerChat, self, AddMemberPayload, AuthPayload, ChatPayload, CreateGroup, CreatePeerChat,
CreateRoomPayload, DeleteRoomPayload, MemberPermission, RemoveMemberPayload, RichText, CreateRoomPayload, DeleteRoomPayload, MemberPermission, RemoveMemberPayload, RichText,
RoomAttrs, ServerPermission, SignedChatMsg, SignedChatMsgWithId, UserRegisterChallengeResponse, RoomAttrs, ServerPermission, SignedChatMsg, SignedChatMsgWithId, UpdateMemberPayload,
UserRegisterPayload, WithMsgId, UserRegisterChallengeResponse, UserRegisterPayload, WithMsgId,
}; };
use blah_types::server::{ use blah_types::server::{
RoomList, RoomMember, RoomMemberList, RoomMetadata, RoomMsgs, ServerEvent, ServerMetadata, RoomList, RoomMember, RoomMemberList, RoomMetadata, RoomMsgs, ServerEvent, ServerMetadata,
@ -346,6 +346,33 @@ impl Server {
.map_ok(|None| {}) .map_ok(|None| {})
} }
fn update_member_perm(
&self,
rid: Id,
act_user: &User,
tgt_user: &User,
permission: MemberPermission,
) -> impl Future<Output = Result<()>> + 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( fn post_chat(
&self, &self,
rid: Id, rid: Id,
@ -1639,7 +1666,7 @@ async fn room_member(server: Server) {
#[rstest] #[rstest]
#[tokio::test] #[tokio::test]
async fn room_management(server: Server) { async fn room_mgmt_remove(server: Server) {
let rid = server let rid = server
.create_room(&ALICE, RoomAttrs::PUBLIC_JOINABLE, "public") .create_room(&ALICE, RoomAttrs::PUBLIC_JOINABLE, "public")
.await .await
@ -1681,3 +1708,60 @@ async fn room_management(server: Server) {
.await .await
.expect_api_err(StatusCode::NOT_FOUND, "room_not_found"); .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();
}

View file

@ -594,7 +594,38 @@ paths:
schema: schema:
$ref: '#/components/schemas/ApiError' $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: delete:
summary: Remove a room member. summary: Remove a room member.
@ -876,6 +907,38 @@ components:
user: user:
type: string 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: Signed-RemoveMember:
type: object type: object
properties: properties: