Kanidm group-gated rooms and minimal mesh calling
Rooms can now require a Kanidm group (via the `groups` OIDC claim, mapped by `oauth2 update-claim-map` server-side) - dev/ops require `developers`, enforced at every message path (send, history, SSE). Adds a minimal WebRTC mesh call feature scoped to the lobby room, signaled over a separate `call.room.*` NATS subject kept out of the chat archive: public STUN only, no TURN, no SFU - small groups on friendly networks, by design.
This commit is contained in:
+33
-9
@@ -3,23 +3,42 @@ use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Rooms available in the UI. Each maps to the NATS subject
|
||||
/// `chat.room.<name>`, so any other NATS client on the bus can join in.
|
||||
pub const ROOMS: &[(&str, &str)] = &[
|
||||
("lobby", "general traffic"),
|
||||
("dev", "build & ship"),
|
||||
("ops", "incidents & infra"),
|
||||
("random", "off the record"),
|
||||
/// The third field is the Kanidm group (via the `groups` OIDC claim,
|
||||
/// see `oauth2 update-claim-map`) required to read/post in that room -
|
||||
/// `None` means open to anyone in `cnats_users`.
|
||||
pub const ROOMS: &[(&str, &str, Option<&str>)] = &[
|
||||
("lobby", "general traffic", None),
|
||||
("dev", "build & ship", Some("developers")),
|
||||
("ops", "incidents & infra", Some("developers")),
|
||||
("random", "off the record", None),
|
||||
];
|
||||
|
||||
pub const DEFAULT_ROOM: &str = "lobby";
|
||||
|
||||
pub fn is_valid_room(room: &str) -> bool {
|
||||
ROOMS.iter().any(|(name, _)| *name == room)
|
||||
ROOMS.iter().any(|(name, _, _)| *name == room)
|
||||
}
|
||||
|
||||
pub fn room_subject(room: &str) -> String {
|
||||
format!("chat.room.{room}")
|
||||
}
|
||||
|
||||
/// Whether `user` may read/post in `room`. `false` for an unknown room -
|
||||
/// callers should check `is_valid_room` separately if they need to tell
|
||||
/// "unknown room" and "not authorized" apart in the error they return.
|
||||
/// Synchronous and I/O-free: the user's groups are already baked into
|
||||
/// their session (from the `groups` OIDC claim at login), so this never
|
||||
/// needs a live Kanidm round-trip - and never gets more current than
|
||||
/// that login until they sign in again.
|
||||
pub fn is_authorized_for_room(user: &crate::auth::User, room: &str) -> bool {
|
||||
ROOMS
|
||||
.iter()
|
||||
.find(|(name, _, _)| *name == room)
|
||||
.is_some_and(|(_, _, required_group)| {
|
||||
required_group.is_none_or(|g| user.groups.iter().any(|ug| ug == g))
|
||||
})
|
||||
}
|
||||
|
||||
/// A single chat message as it travels over NATS (JSON-encoded payload).
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct ChatMessage {
|
||||
@@ -62,6 +81,9 @@ pub async fn send_message(room: String, text: String) -> Result<(), ServerFnErro
|
||||
else {
|
||||
return Err(ServerFnError::new("not signed in"));
|
||||
};
|
||||
if !is_authorized_for_room(&user, &room) {
|
||||
return Err(ServerFnError::new("not authorized for this room"));
|
||||
}
|
||||
|
||||
let state = expect_context::<AppState>();
|
||||
let now = chrono::Utc::now();
|
||||
@@ -94,13 +116,15 @@ pub async fn room_history(room: String) -> Result<Vec<ChatMessage>, ServerFnErro
|
||||
return Err(ServerFnError::new("unknown room"));
|
||||
}
|
||||
let session: tower_sessions::Session = leptos_axum::extract().await?;
|
||||
if session
|
||||
let Some(user) = session
|
||||
.get::<User>(SESSION_USER_KEY)
|
||||
.await
|
||||
.map_err(|e| ServerFnError::new(e.to_string()))?
|
||||
.is_none()
|
||||
{
|
||||
else {
|
||||
return Err(ServerFnError::new("not signed in"));
|
||||
};
|
||||
if !is_authorized_for_room(&user, &room) {
|
||||
return Err(ServerFnError::new("not authorized for this room"));
|
||||
}
|
||||
|
||||
let state = expect_context::<AppState>();
|
||||
|
||||
Reference in New Issue
Block a user