Allow multiple initial members on room creation

This commit is contained in:
oxalica 2024-08-29 19:57:43 -04:00
parent cf5d648315
commit ff89d36ee5
3 changed files with 87 additions and 21 deletions

View file

@ -4,7 +4,10 @@ use std::{fs, io};
use anyhow::{Context, Result};
use bitflags::Flags;
use blah::types::{ChatPayload, CreateRoomPayload, RoomAttrs, ServerPermission, UserKey, WithSig};
use blah::types::{
ChatPayload, CreateRoomPayload, MemberPermission, RoomAttrs, RoomMember, RoomMemberList,
ServerPermission, UserKey, WithSig,
};
use ed25519_dalek::pkcs8::spki::der::pem::LineEnding;
use ed25519_dalek::pkcs8::{DecodePrivateKey, DecodePublicKey, EncodePrivateKey, EncodePublicKey};
use ed25519_dalek::{SigningKey, VerifyingKey, PUBLIC_KEY_LENGTH};
@ -208,8 +211,14 @@ async fn main_api(api_url: Url, command: ApiCommand) -> Result<()> {
} => {
let key = load_signing_key(&private_key_file)?;
let payload = CreateRoomPayload {
title,
attrs: attrs.unwrap_or_default(),
title,
// The CLI does not support passing multiple members because `User` itself is a
// disjoint arg-group.
members: RoomMemberList(vec![RoomMember {
permission: MemberPermission::ALL,
user: UserKey(key.verifying_key().to_bytes()),
}]),
};
let payload = WithSig::sign(&key, &mut OsRng, payload)?;

View file

@ -12,7 +12,7 @@ use axum::response::{sse, IntoResponse, Response};
use axum::routing::{get, post};
use axum::{async_trait, Json, Router};
use blah::types::{
AuthPayload, ChatItem, ChatPayload, CreateRoomPayload, RoomAttrs, RoomPermission,
AuthPayload, ChatItem, ChatPayload, CreateRoomPayload, MemberPermission, RoomAttrs,
ServerPermission, Signee, UserKey, WithSig,
};
use ed25519_dalek::SIGNATURE_LENGTH;
@ -128,24 +128,30 @@ async fn room_create(
st: ArcState,
SignedJson(params): SignedJson<CreateRoomPayload>,
) -> Result<Json<Uuid>, StatusCode> {
let members = &params.signee.payload.members.0;
if !members
.iter()
.any(|m| m.user == params.signee.user && m.permission == MemberPermission::ALL)
{
return Err(StatusCode::BAD_REQUEST);
}
let mut conn = st.conn.lock().unwrap();
let Some((uid, _perm)) = conn
let Some(true) = conn
.query_row(
r"
SELECT `uid`, `permission`
SELECT `permission`
FROM `user`
WHERE `userkey` = ?
",
params![params.signee.user],
|row| {
let uid = row.get::<_, u64>("uid")?;
let perm = row.get::<_, ServerPermission>("permission")?;
Ok((uid, perm))
Ok(perm.contains(ServerPermission::CREATE_ROOM))
},
)
.optional()
.map_err(from_db_error)?
.filter(|(_, perm)| perm.contains(ServerPermission::CREATE_ROOM))
else {
return Err(StatusCode::FORBIDDEN);
};
@ -166,17 +172,31 @@ async fn room_create(
},
|row| row.get::<_, u64>(0),
)?;
txn.execute(
let mut insert_user = txn.prepare(
r"
INSERT INTO `user` (`userkey`)
VALUES (?)
ON CONFLICT (`userkey`) DO NOTHING
",
)?;
let mut insert_member = txn.prepare(
r"
INSERT INTO `room_member` (`rid`, `uid`, `permission`)
VALUES (:rid, :uid, :permission)
SELECT :rid, `uid`, :permission
FROM `user`
WHERE `userkey` = :userkey
",
named_params! {
":rid": rid,
":uid": uid,
":permission": RoomPermission::ALL,
},
)?;
for member in members {
insert_user.execute(params![member.user])?;
insert_member.execute(named_params! {
":rid": rid,
":userkey": member.user,
":permission": member.permission,
})?;
}
drop(insert_member);
drop(insert_user);
txn.commit()?;
Ok(())
})()
@ -458,7 +478,7 @@ async fn room_post_item(
named_params! {
":ruuid": ruuid,
":userkey": &chat.signee.user,
":perm": RoomPermission::POST_CHAT,
":perm": MemberPermission::POST_CHAT,
},
|row| Ok((row.get::<_, u64>("rid")?, row.get::<_, u64>("uid")?)),
)

View file

@ -17,7 +17,7 @@ use uuid::Uuid;
const TIMESTAMP_TOLERENCE: u64 = 90;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(transparent)]
pub struct UserKey(#[serde(with = "hex::serde")] pub [u8; PUBLIC_KEY_LENGTH]);
@ -93,9 +93,46 @@ pub type ChatItem = WithSig<ChatPayload>;
#[serde(tag = "typ", rename = "create_room")]
pub struct CreateRoomPayload {
pub attrs: RoomAttrs,
/// The initial member list. Besides invariants of `RoomMemberList`, this also must include the
/// room creator themselves, with the highest permission (-1).
pub members: RoomMemberList,
pub title: String,
}
/// A collection of room members, with these invariants:
/// 1. Sorted by userkeys.
/// 2. No duplicated users.
#[derive(Debug, Deserialize)]
#[serde(try_from = "Vec<RoomMember>")]
pub struct RoomMemberList(pub Vec<RoomMember>);
impl Serialize for RoomMemberList {
fn serialize<S>(&self, ser: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
self.0.serialize(ser)
}
}
impl TryFrom<Vec<RoomMember>> for RoomMemberList {
type Error = &'static str;
fn try_from(members: Vec<RoomMember>) -> Result<Self, Self::Error> {
if members.windows(2).all(|w| w[0].user.0 < w[1].user.0) {
Ok(Self(members))
} else {
Err("unsorted or duplicated users")
}
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct RoomMember {
pub permission: MemberPermission,
pub user: UserKey,
}
/// Proof of room membership for read-access.
///
/// TODO: Should we use JWT here instead?
@ -107,7 +144,7 @@ pub struct AuthPayload {}
#[serde(deny_unknown_fields, tag = "typ", rename_all = "snake_case")]
pub enum RoomAdminPayload {
AddMember {
permission: RoomPermission,
permission: MemberPermission,
room: Uuid,
user: UserKey,
},
@ -123,7 +160,7 @@ bitflags! {
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RoomPermission: u64 {
pub struct MemberPermission: u64 {
const POST_CHAT = 1 << 0;
const ADD_MEMBER = 1 << 1;
@ -139,7 +176,7 @@ bitflags! {
}
impl_serde_for_bitflags!(ServerPermission);
impl_serde_for_bitflags!(RoomPermission);
impl_serde_for_bitflags!(MemberPermission);
impl_serde_for_bitflags!(RoomAttrs);
mod sql_impl {
@ -184,7 +221,7 @@ mod sql_impl {
};
}
impl_u64_flag!(ServerPermission, RoomPermission, RoomAttrs);
impl_u64_flag!(ServerPermission, MemberPermission, RoomAttrs);
}
#[cfg(test)]