feat(cli): allow system room visibility updates (#420)

## Summary

- allow `synctv room visibility` to omit the actor and run as an
authenticated management-plane operation
- preserve optional `--username`, `--user-id`, and `--email` actor
selection for normal room permission enforcement
- keep the permission bypass confined to the management service and
reuse the same visibility update, cache invalidation, and guest
revocation behavior

Follow-up to #419.

## Validation

- `cargo fmt --all -- --check`
- `cargo check -p synctv-core -p synctv-management -p synctv`
- `cargo clippy -p synctv-core -p synctv-management -p synctv
--all-targets -- -D warnings`
- `cargo test -p synctv room_visibility` (3 passed)
- verified `synctv room visibility --help` shows actor selectors as
optional and still requires exactly one of `--public` or `--private`
pull/421/head
zijiren 1 month ago committed by GitHub
parent 198cd5a9cb
commit c80f660637
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -17,6 +17,25 @@ impl RoomService {
.check_permission_no_cache(room_id, user_id, RoomPermission::MANAGE_ROOM_SETTINGS)
.await?;
self.update_room_visibility_unchecked(room_id, is_public)
.await
}
/// Update visibility from an already authenticated management/system plane.
pub async fn admin_update_room_visibility(
&self,
room_id: &RoomId,
is_public: bool,
) -> Result<Room> {
self.update_room_visibility_unchecked(room_id, is_public)
.await
}
async fn update_room_visibility_unchecked(
&self,
room_id: &RoomId,
is_public: bool,
) -> Result<Room> {
let mut tx = self.pool.begin().await?;
let mut room = self
.room_repo

@ -5422,6 +5422,12 @@ async fn test_update_room_visibility_revokes_guest_access_and_enforces_permissio
.is_err(),
"users without room settings permission must not change visibility"
);
let system_updated = room_service
.admin_update_room_visibility(&room.id, true)
.await
.checked("authenticated management plane should change visibility");
assert!(system_updated.is_public);
}
#[tokio::test]

@ -2825,14 +2825,23 @@ impl ManagementService for ManagementServiceImpl {
request: Request<UpdateRoomVisibilityRequest>,
) -> Result<Response<client_proto::Room>, Status> {
self.check_admin_get_validated(&request)?;
let req = request.into_inner();
let actor_user_id = self.resolve_client_actor_user_id(req.actor).await?;
let room_id = room_id_from_public(&req.room_id, &self.public_id_codec)?;
let room = self
.room_service
.update_room_visibility(&room_id, &actor_user_id, req.is_public)
.await
.map_err(Self::map_room_access_error)?;
let UpdateRoomVisibilityRequest {
room_id: public_room_id,
actor,
is_public,
} = request.into_inner();
let room_id = room_id_from_public(&public_room_id, &self.public_id_codec)?;
let room = if let Some(actor) = actor {
let actor_user_id = self.resolve_client_actor_user_id(Some(actor)).await?;
self.room_service
.update_room_visibility(&room_id, &actor_user_id, is_public)
.await
} else {
self.room_service
.admin_update_room_visibility(&room_id, is_public)
.await
}
.map_err(Self::map_room_access_error)?;
self.room_cache_fanout.publish_invalidation(&room_id);
self.client_room_response(&room).await.map(Response::new)
}

@ -120,7 +120,7 @@ pub struct RoomVisibilityArgs {
pub room_id: String,
#[command(flatten)]
pub actor: ActorUserArgs,
pub actor: OptionalActorUserArgs,
/// List the room in discovery and allow anonymous guest access
#[arg(long = "public", group = "room_visibility")]

@ -70,7 +70,7 @@ pub(super) async fn execute_room(room_command: RoomCommand) -> Result<()> {
update_room_visibility,
management_proto::UpdateRoomVisibilityRequest {
room_id: args.room_id,
actor: Some(args.actor.to_management_proto()?),
actor: args.actor.to_management_proto()?,
is_public,
}
)?;

@ -1064,6 +1064,23 @@ fn cli_requires_exactly_one_room_visibility_flag() {
}
}
#[test]
fn cli_allows_system_room_visibility_operation_without_actor() {
let cli = Cli::parse_from(["synctv", "room", "visibility", "room-123", "--private"]);
match cli.command {
Commands::Room(RoomCommand {
command: RoomSubcommand::Visibility(args),
..
}) => {
assert!(args.actor.username.is_none());
assert!(args.actor.user_id.is_none());
assert!(args.actor.email.is_none());
assert!(!args.is_public());
}
other => panic!("unexpected command parsed: {other:?}"),
}
}
#[test]
fn room_create_settings_json_is_applied_as_patch_to_defaults() {
let patch: synctv_proto::client::RoomSettingsPatch =

Loading…
Cancel
Save