matrix-rtc: separate standalone publication from room creation
This commit is contained in:
parent
56120933f3
commit
2ff4caa788
5 changed files with 321 additions and 1 deletions
171
services/matrix-rtc-jwt/standalone-publication.patch
Normal file
171
services/matrix-rtc-jwt/standalone-publication.patch
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
diff --git a/integration-tests/tests/get_token.rs b/integration-tests/tests/get_token.rs
|
||||
index 386f97b..102ff4a 100644
|
||||
--- a/integration-tests/tests/get_token.rs
|
||||
+++ b/integration-tests/tests/get_token.rs
|
||||
@@ -353,13 +353,12 @@ async fn remote_token() {
|
||||
assert_eq!(response["url"].as_str(), Some(sfu.url()));
|
||||
let jwt = response["jwt"].as_str().unwrap_or_default();
|
||||
|
||||
- // The JWT should grant joining, subscribing and updating the
|
||||
- // participant's own metadata, but not publishing, since the homeserver
|
||||
- // is remote.
|
||||
+ // Remote verified users may publish, but the service must not create a
|
||||
+ // room for them. Room creation remains gated on the full-access list.
|
||||
let claims = decode_livekit_jwt(jwt);
|
||||
assert_eq!(claims["iss"].as_str(), Some(LIVEKIT_KEY));
|
||||
assert_eq!(claims["video"]["roomJoin"].as_bool(), Some(true));
|
||||
- assert_eq!(claims["video"]["canPublish"].as_bool(), Some(false));
|
||||
+ assert_eq!(claims["video"]["canPublish"].as_bool(), Some(true));
|
||||
assert_eq!(claims["video"]["canSubscribe"].as_bool(), Some(true));
|
||||
assert_eq!(
|
||||
claims["video"]["canUpdateOwnMetadata"].as_bool(),
|
||||
diff --git a/integration-tests/tests/sfu_get.rs b/integration-tests/tests/sfu_get.rs
|
||||
index 5a08445..2cc8672 100644
|
||||
--- a/integration-tests/tests/sfu_get.rs
|
||||
+++ b/integration-tests/tests/sfu_get.rs
|
||||
@@ -323,7 +323,7 @@ async fn remote_token() {
|
||||
let claims = decode_livekit_jwt(jwt);
|
||||
assert_eq!(claims["iss"].as_str(), Some(LIVEKIT_KEY));
|
||||
assert_eq!(claims["video"]["roomJoin"].as_bool(), Some(true));
|
||||
- assert_eq!(claims["video"]["canPublish"].as_bool(), Some(false));
|
||||
+ assert_eq!(claims["video"]["canPublish"].as_bool(), Some(true));
|
||||
assert_eq!(claims["video"]["canSubscribe"].as_bool(), Some(true));
|
||||
assert_eq!(
|
||||
claims["sub"].as_str(),
|
||||
diff --git a/src/handler.rs b/src/handler.rs
|
||||
index 2797194..7910e1e 100644
|
||||
--- a/src/handler.rs
|
||||
+++ b/src/handler.rs
|
||||
@@ -978,7 +978,11 @@ impl Handler {
|
||||
&lk_room_alias,
|
||||
&lk_identity,
|
||||
&matrix_id,
|
||||
- is_full_access_user,
|
||||
+ // Publication is an authenticated participation permission, not a
|
||||
+ // room-creation privilege. OpenID and (when configured) membership
|
||||
+ // checks above must still succeed. Keep creation/delegation gated
|
||||
+ // below and LiveKit room.auto_create=false on the SFU.
|
||||
+ true,
|
||||
)?;
|
||||
|
||||
if is_full_access_user {
|
||||
@@ -1058,7 +1062,11 @@ impl Handler {
|
||||
&lk_room_alias,
|
||||
&lk_identity,
|
||||
&matrix_id,
|
||||
- is_full_access_user,
|
||||
+ // Publication is an authenticated participation permission, not a
|
||||
+ // room-creation privilege. OpenID and (when configured) membership
|
||||
+ // checks above must still succeed. Keep creation/delegation gated
|
||||
+ // below and LiveKit room.auto_create=false on the SFU.
|
||||
+ true,
|
||||
)?;
|
||||
|
||||
if is_full_access_user {
|
||||
diff --git a/src/handler_tests.rs b/src/handler_tests.rs
|
||||
index 54d1650..e6c943c 100644
|
||||
--- a/src/handler_tests.rs
|
||||
+++ b/src/handler_tests.rs
|
||||
@@ -5403,3 +5403,87 @@ async fn test_membership_skipped_unless_hs_is_the_only_full_access_homeserver()
|
||||
handler.close().await;
|
||||
}
|
||||
}
|
||||
+
|
||||
+// iscg standalone publication policy regression tests. The full-access list
|
||||
+// governs room creation/delegation, never whether a verified participant speaks.
|
||||
+#[tokio::test]
|
||||
+async fn test_iscg_standalone_publication_does_not_grant_creation() {
|
||||
+ for path in ["/sfu/get", "/get_token"] {
|
||||
+ for server in ["example.com", "astr.to", "unrelated.example"] {
|
||||
+ let sub = format!("@user:{server}");
|
||||
+ let created = Arc::new(AtomicUsize::new(0));
|
||||
+ let counter = created.clone();
|
||||
+ let deps = HandlerTestDeps {
|
||||
+ exchange_openid_userinfo_fn: exchange_openid_userinfo_ok(&sub),
|
||||
+ create_livekit_room_fn: Some(Box::new(move |_, _, _| {
|
||||
+ counter.fetch_add(1, Ordering::SeqCst);
|
||||
+ Ok(())
|
||||
+ })),
|
||||
+ ..Default::default()
|
||||
+ };
|
||||
+ let handler = new_get_token_handler(deps);
|
||||
+ let mut req = valid_sfu_request();
|
||||
+ req.openid_token.matrix_server_name = server.into();
|
||||
+ req.member.claimed_user_id = sub.clone();
|
||||
+ let body = if path == "/get_token" {
|
||||
+ serde_json::to_string(&req).unwrap()
|
||||
+ } else {
|
||||
+ serde_json::to_string(&LegacySfuRequest {
|
||||
+ room: req.room_id.clone(),
|
||||
+ openid_token: req.openid_token.clone(),
|
||||
+ device_id: req.member.claimed_device_id.clone(),
|
||||
+ ..Default::default()
|
||||
+ }).unwrap()
|
||||
+ };
|
||||
+ let resp = send_request(&handler, post_json(path, body)).await;
|
||||
+ assert_eq!(resp.status(), StatusCode::OK, "{path} {server}");
|
||||
+ let result: SfuResponse = serde_json::from_slice(&body_bytes(resp).await).unwrap();
|
||||
+ let claims = parse_jwt_claims(&result.jwt, &handler.livekit_auth.secret);
|
||||
+ assert_eq!(claims["video"]["canPublish"], true, "{path} {server}");
|
||||
+ assert_eq!(claims["video"]["canSubscribe"], true);
|
||||
+ assert_eq!(claims["video"]["roomJoin"], true);
|
||||
+ for privilege in ["roomCreate", "roomAdmin", "roomList", "roomRecord", "ingressAdmin"] {
|
||||
+ assert_ne!(claims["video"][privilege], true, "{path}: {privilege}");
|
||||
+ }
|
||||
+ let want_room = livekit_room_alias_for(&req.room_id, &req.slot_id);
|
||||
+ assert_eq!(claims["video"]["room"], want_room.0);
|
||||
+ assert_eq!(created.load(Ordering::SeqCst), usize::from(server == "example.com"));
|
||||
+ assert_eq!(handler.is_full_access_user(server), server == "example.com");
|
||||
+ handler.close().await;
|
||||
+ }
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
+#[tokio::test]
|
||||
+async fn test_iscg_publication_still_requires_valid_openid() {
|
||||
+ for path in ["/sfu/get", "/get_token"] {
|
||||
+ for forged_subject in [false, true] {
|
||||
+ let deps = HandlerTestDeps {
|
||||
+ exchange_openid_userinfo_fn: if forged_subject {
|
||||
+ exchange_openid_userinfo_ok("@victim:example.com")
|
||||
+ } else {
|
||||
+ Some(Box::new(|_| Err("invalid or expired token".into())))
|
||||
+ },
|
||||
+ // An accidental room creation or membership request panics.
|
||||
+ ..Default::default()
|
||||
+ };
|
||||
+ let handler = new_get_token_handler(deps);
|
||||
+ let mut req = valid_sfu_request();
|
||||
+ req.openid_token.matrix_server_name = "astr.to".into();
|
||||
+ req.member.claimed_user_id = "@user:astr.to".into();
|
||||
+ let body = if path == "/get_token" {
|
||||
+ serde_json::to_string(&req).unwrap()
|
||||
+ } else {
|
||||
+ serde_json::to_string(&LegacySfuRequest {
|
||||
+ room: req.room_id,
|
||||
+ openid_token: req.openid_token,
|
||||
+ device_id: "DEVICE".into(),
|
||||
+ ..Default::default()
|
||||
+ }).unwrap()
|
||||
+ };
|
||||
+ let resp = send_request(&handler, post_json(path, body)).await;
|
||||
+ assert_eq!(resp.status(), StatusCode::UNAUTHORIZED, "{path} forged={forged_subject}");
|
||||
+ handler.close().await;
|
||||
+ }
|
||||
+ }
|
||||
+}
|
||||
diff --git a/src/main.rs b/src/main.rs
|
||||
index 2a057d6..7e7ebfd 100644
|
||||
--- a/src/main.rs
|
||||
+++ b/src/main.rs
|
||||
@@ -49,6 +49,10 @@ async fn main() {
|
||||
}
|
||||
}
|
||||
|
||||
+ info!(
|
||||
+ "iscg standalone policy v1: verified users may publish; room creation and delayed-leave delegation remain full-access only"
|
||||
+ );
|
||||
+
|
||||
let config = match parse_config() {
|
||||
Ok(config) => config,
|
||||
Err(err) => {
|
||||
Loading…
Add table
Add a link
Reference in a new issue