mirror of
https://github.com/Blah-IM/blahrs.git
synced 2025-05-01 08:41:09 +00:00
Allow multiple initial members on room creation
This commit is contained in:
parent
cf5d648315
commit
ff89d36ee5
3 changed files with 87 additions and 21 deletions
|
@ -4,7 +4,10 @@ use std::{fs, io};
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use bitflags::Flags;
|
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::spki::der::pem::LineEnding;
|
||||||
use ed25519_dalek::pkcs8::{DecodePrivateKey, DecodePublicKey, EncodePrivateKey, EncodePublicKey};
|
use ed25519_dalek::pkcs8::{DecodePrivateKey, DecodePublicKey, EncodePrivateKey, EncodePublicKey};
|
||||||
use ed25519_dalek::{SigningKey, VerifyingKey, PUBLIC_KEY_LENGTH};
|
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 key = load_signing_key(&private_key_file)?;
|
||||||
let payload = CreateRoomPayload {
|
let payload = CreateRoomPayload {
|
||||||
title,
|
|
||||||
attrs: attrs.unwrap_or_default(),
|
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)?;
|
let payload = WithSig::sign(&key, &mut OsRng, payload)?;
|
||||||
|
|
||||||
|
|
48
src/main.rs
48
src/main.rs
|
@ -12,7 +12,7 @@ use axum::response::{sse, IntoResponse, Response};
|
||||||
use axum::routing::{get, post};
|
use axum::routing::{get, post};
|
||||||
use axum::{async_trait, Json, Router};
|
use axum::{async_trait, Json, Router};
|
||||||
use blah::types::{
|
use blah::types::{
|
||||||
AuthPayload, ChatItem, ChatPayload, CreateRoomPayload, RoomAttrs, RoomPermission,
|
AuthPayload, ChatItem, ChatPayload, CreateRoomPayload, MemberPermission, RoomAttrs,
|
||||||
ServerPermission, Signee, UserKey, WithSig,
|
ServerPermission, Signee, UserKey, WithSig,
|
||||||
};
|
};
|
||||||
use ed25519_dalek::SIGNATURE_LENGTH;
|
use ed25519_dalek::SIGNATURE_LENGTH;
|
||||||
|
@ -128,24 +128,30 @@ async fn room_create(
|
||||||
st: ArcState,
|
st: ArcState,
|
||||||
SignedJson(params): SignedJson<CreateRoomPayload>,
|
SignedJson(params): SignedJson<CreateRoomPayload>,
|
||||||
) -> Result<Json<Uuid>, StatusCode> {
|
) -> Result<Json<Uuid>, StatusCode> {
|
||||||
|
let members = ¶ms.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 mut conn = st.conn.lock().unwrap();
|
||||||
let Some((uid, _perm)) = conn
|
let Some(true) = conn
|
||||||
.query_row(
|
.query_row(
|
||||||
r"
|
r"
|
||||||
SELECT `uid`, `permission`
|
SELECT `permission`
|
||||||
FROM `user`
|
FROM `user`
|
||||||
WHERE `userkey` = ?
|
WHERE `userkey` = ?
|
||||||
",
|
",
|
||||||
params![params.signee.user],
|
params![params.signee.user],
|
||||||
|row| {
|
|row| {
|
||||||
let uid = row.get::<_, u64>("uid")?;
|
|
||||||
let perm = row.get::<_, ServerPermission>("permission")?;
|
let perm = row.get::<_, ServerPermission>("permission")?;
|
||||||
Ok((uid, perm))
|
Ok(perm.contains(ServerPermission::CREATE_ROOM))
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.optional()
|
.optional()
|
||||||
.map_err(from_db_error)?
|
.map_err(from_db_error)?
|
||||||
.filter(|(_, perm)| perm.contains(ServerPermission::CREATE_ROOM))
|
|
||||||
else {
|
else {
|
||||||
return Err(StatusCode::FORBIDDEN);
|
return Err(StatusCode::FORBIDDEN);
|
||||||
};
|
};
|
||||||
|
@ -166,17 +172,31 @@ async fn room_create(
|
||||||
},
|
},
|
||||||
|row| row.get::<_, u64>(0),
|
|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"
|
r"
|
||||||
INSERT INTO `room_member` (`rid`, `uid`, `permission`)
|
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()?;
|
txn.commit()?;
|
||||||
Ok(())
|
Ok(())
|
||||||
})()
|
})()
|
||||||
|
@ -458,7 +478,7 @@ async fn room_post_item(
|
||||||
named_params! {
|
named_params! {
|
||||||
":ruuid": ruuid,
|
":ruuid": ruuid,
|
||||||
":userkey": &chat.signee.user,
|
":userkey": &chat.signee.user,
|
||||||
":perm": RoomPermission::POST_CHAT,
|
":perm": MemberPermission::POST_CHAT,
|
||||||
},
|
},
|
||||||
|row| Ok((row.get::<_, u64>("rid")?, row.get::<_, u64>("uid")?)),
|
|row| Ok((row.get::<_, u64>("rid")?, row.get::<_, u64>("uid")?)),
|
||||||
)
|
)
|
||||||
|
|
47
src/types.rs
47
src/types.rs
|
@ -17,7 +17,7 @@ use uuid::Uuid;
|
||||||
|
|
||||||
const TIMESTAMP_TOLERENCE: u64 = 90;
|
const TIMESTAMP_TOLERENCE: u64 = 90;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
#[serde(transparent)]
|
#[serde(transparent)]
|
||||||
pub struct UserKey(#[serde(with = "hex::serde")] pub [u8; PUBLIC_KEY_LENGTH]);
|
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")]
|
#[serde(tag = "typ", rename = "create_room")]
|
||||||
pub struct CreateRoomPayload {
|
pub struct CreateRoomPayload {
|
||||||
pub attrs: RoomAttrs,
|
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,
|
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.
|
/// Proof of room membership for read-access.
|
||||||
///
|
///
|
||||||
/// TODO: Should we use JWT here instead?
|
/// TODO: Should we use JWT here instead?
|
||||||
|
@ -107,7 +144,7 @@ pub struct AuthPayload {}
|
||||||
#[serde(deny_unknown_fields, tag = "typ", rename_all = "snake_case")]
|
#[serde(deny_unknown_fields, tag = "typ", rename_all = "snake_case")]
|
||||||
pub enum RoomAdminPayload {
|
pub enum RoomAdminPayload {
|
||||||
AddMember {
|
AddMember {
|
||||||
permission: RoomPermission,
|
permission: MemberPermission,
|
||||||
room: Uuid,
|
room: Uuid,
|
||||||
user: UserKey,
|
user: UserKey,
|
||||||
},
|
},
|
||||||
|
@ -123,7 +160,7 @@ bitflags! {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub struct RoomPermission: u64 {
|
pub struct MemberPermission: u64 {
|
||||||
const POST_CHAT = 1 << 0;
|
const POST_CHAT = 1 << 0;
|
||||||
const ADD_MEMBER = 1 << 1;
|
const ADD_MEMBER = 1 << 1;
|
||||||
|
|
||||||
|
@ -139,7 +176,7 @@ bitflags! {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl_serde_for_bitflags!(ServerPermission);
|
impl_serde_for_bitflags!(ServerPermission);
|
||||||
impl_serde_for_bitflags!(RoomPermission);
|
impl_serde_for_bitflags!(MemberPermission);
|
||||||
impl_serde_for_bitflags!(RoomAttrs);
|
impl_serde_for_bitflags!(RoomAttrs);
|
||||||
|
|
||||||
mod sql_impl {
|
mod sql_impl {
|
||||||
|
@ -184,7 +221,7 @@ mod sql_impl {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
impl_u64_flag!(ServerPermission, RoomPermission, RoomAttrs);
|
impl_u64_flag!(ServerPermission, MemberPermission, RoomAttrs);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|
Loading…
Add table
Reference in a new issue