mirror of
https://github.com/Blah-IM/blahrs.git
synced 2025-05-01 00:31:09 +00:00
feat(webapi): impl member permission update
This commit is contained in:
parent
ad4a38cf43
commit
8378c4d230
5 changed files with 212 additions and 7 deletions
|
@ -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();
|
||||||
|
|
|
@ -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!(
|
||||||
|
|
|
@ -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>>,
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
|
@ -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:
|
||||||
|
|
Loading…
Add table
Reference in a new issue