Add "permission" for sessions that may not receive display names.

By default, the "join" events contain the whole userdata as received
from the "auth" response. This might include a "displayname" field
containing the display name of the associated user.

For privacy reasons in certain scenarios, some attendees should not
be able to know which other users are connected to a room, therefore
the "displayname" should be hidden for them. By default, no filtering
occurrs.
This commit is contained in:
Joachim Bauch 2022-04-11 11:06:58 +02:00
parent 6fa4a8b434
commit 39cc523477
No known key found for this signature in database
GPG Key ID: 77C1D22D53E15F02
4 changed files with 232 additions and 17 deletions

View File

@ -620,6 +620,18 @@ type RoomFlagsServerMessage struct {
Flags uint32 `json:"flags"`
}
type ChatComment map[string]interface{}
type RoomEventMessageDataChat struct {
Comment *ChatComment `json:"comment,omitempty"`
}
type RoomEventMessageData struct {
Type string `json:"type"`
Chat *RoomEventMessageDataChat `json:"chat,omitempty"`
}
type EventServerMessage struct {
Target string `json:"target"`
Type string `json:"type"`
@ -646,6 +658,15 @@ type EventServerMessageSessionEntry struct {
RoomSessionId string `json:"roomsessionid,omitempty"`
}
func (e *EventServerMessageSessionEntry) Clone() *EventServerMessageSessionEntry {
return &EventServerMessageSessionEntry{
SessionId: e.SessionId,
UserId: e.UserId,
User: e.User,
RoomSessionId: e.RoomSessionId,
}
}
// MCU-related types
type AnswerOfferMessage struct {

View File

@ -214,6 +214,9 @@ func (s *ClientSession) hasAnyPermissionLocked(permission ...Permission) bool {
func (s *ClientSession) hasPermissionLocked(permission Permission) bool {
if !s.supportsPermissions {
// Old-style session that doesn't receive permissions from Nextcloud.
if result, found := DefaultPermissionOverrides[permission]; found {
return result
}
return true
}
@ -641,6 +644,11 @@ func (s *ClientSession) SendError(e *Error) bool {
}
func (s *ClientSession) SendMessage(message *ServerMessage) bool {
message = s.filterMessage(message)
if message == nil {
return true
}
s.mu.Lock()
defer s.mu.Unlock()
@ -1026,6 +1034,110 @@ func (s *ClientSession) storePendingMessage(message *ServerMessage) {
}
}
func filterDisplayNames(events []*EventServerMessageSessionEntry) []*EventServerMessageSessionEntry {
result := make([]*EventServerMessageSessionEntry, 0, len(events))
for _, event := range events {
if event.User == nil {
result = append(result, event)
continue
}
var userdata map[string]interface{}
if err := json.Unmarshal(*event.User, &userdata); err != nil {
result = append(result, event)
continue
}
if _, found := userdata["displayname"]; !found {
result = append(result, event)
continue
}
delete(userdata, "displayname")
if len(userdata) == 0 {
// No more userdata, no need to serialize empty map.
e := event.Clone()
e.User = nil
result = append(result, e)
continue
}
data, err := json.Marshal(userdata)
if err != nil {
result = append(result, event)
continue
}
e := event.Clone()
e.User = (*json.RawMessage)(&data)
result = append(result, e)
}
return result
}
func (s *ClientSession) filterMessage(message *ServerMessage) *ServerMessage {
switch message.Type {
case "event":
switch message.Event.Target {
case "participants":
if message.Event.Type == "update" {
m := message.Event.Update
users := make(map[string]bool)
for _, entry := range m.Users {
users[entry["sessionId"].(string)] = true
}
for _, entry := range m.Changed {
if users[entry["sessionId"].(string)] {
continue
}
m.Users = append(m.Users, entry)
}
// TODO(jojo): Only send all users if current session id has
// changed its "inCall" flag to true.
m.Changed = nil
}
case "room":
switch message.Event.Type {
case "join":
if s.HasPermission(PERMISSION_HIDE_DISPLAYNAMES) {
message.Event.Join = filterDisplayNames(message.Event.Join)
}
case "message":
if message.Event.Message == nil || message.Event.Message.Data == nil || len(*message.Event.Message.Data) == 0 || !s.HasPermission(PERMISSION_HIDE_DISPLAYNAMES) {
return message
}
var data RoomEventMessageData
if err := json.Unmarshal(*message.Event.Message.Data, &data); err != nil {
return message
}
if data.Type == "chat" && data.Chat != nil && data.Chat.Comment != nil {
if displayName, found := (*data.Chat.Comment)["actorDisplayName"]; found && displayName != "" {
(*data.Chat.Comment)["actorDisplayName"] = ""
if encoded, err := json.Marshal(data); err == nil {
message.Event.Message.Data = (*json.RawMessage)(&encoded)
}
}
}
}
}
case "message":
if message.Message != nil && message.Message.Data != nil && len(*message.Message.Data) > 0 && s.HasPermission(PERMISSION_HIDE_DISPLAYNAMES) {
var data MessageServerMessageData
if err := json.Unmarshal(*message.Message.Data, &data); err != nil {
return message
}
if data.Type == "nickChanged" {
return nil
}
}
}
return message
}
func (s *ClientSession) processNatsMessage(msg *NatsMessage) *ServerMessage {
switch msg.Type {
case "message":
@ -1050,23 +1162,7 @@ func (s *ClientSession) processNatsMessage(msg *NatsMessage) *ServerMessage {
return nil
}
case "event":
if msg.Message.Event.Target == "participants" &&
msg.Message.Event.Type == "update" {
m := msg.Message.Event.Update
users := make(map[string]bool)
for _, entry := range m.Users {
users[entry["sessionId"].(string)] = true
}
for _, entry := range m.Changed {
if users[entry["sessionId"].(string)] {
continue
}
m.Users = append(m.Users, entry)
}
// TODO(jojo): Only send all users if current session id has
// changed its "inCall" flag to true.
m.Changed = nil
} else if msg.Message.Event.Target == "room" {
if msg.Message.Event.Target == "room" {
// Can happen mostly during tests where an older room NATS message
// could be received by a subscriber that joined after it was sent.
if msg.SendTime.Before(s.getRoomJoinTime()) {

View File

@ -259,6 +259,14 @@ func processAuthRequest(t *testing.T, w http.ResponseWriter, r *http.Request, re
UserId: params.UserId,
},
}
userdata := map[string]string{
"displayname": "Displayname " + params.UserId,
}
if data, err := json.Marshal(userdata); err != nil {
t.Fatal(err)
} else {
response.Auth.User = (*json.RawMessage)(&data)
}
return response
}
@ -1827,6 +1835,88 @@ func TestJoinMultiple(t *testing.T) {
}
}
func TestJoinMultipleDisplaynamesPermission(t *testing.T) {
hub, _, _, server := CreateHubForTest(t)
client1 := NewTestClient(t, server, hub)
defer client1.CloseWithBye()
if err := client1.SendHello(testDefaultUserId + "1"); err != nil {
t.Fatal(err)
}
client2 := NewTestClient(t, server, hub)
defer client2.CloseWithBye()
if err := client2.SendHello(testDefaultUserId + "2"); err != nil {
t.Fatal(err)
}
ctx, cancel := context.WithTimeout(context.Background(), testTimeout)
defer cancel()
hello1, err := client1.RunUntilHello(ctx)
if err != nil {
t.Fatal(err)
}
hello2, err := client2.RunUntilHello(ctx)
if err != nil {
t.Fatal(err)
}
session2 := hub.GetSessionByPublicId(hello2.Hello.SessionId).(*ClientSession)
if session2 == nil {
t.Fatalf("Session %s does not exist", hello2.Hello.SessionId)
}
// Client 2 may not receive display names.
session2.SetPermissions([]Permission{PERMISSION_HIDE_DISPLAYNAMES})
// Join room by id (first client).
roomId := "test-room"
if room, err := client1.JoinRoom(ctx, roomId); err != nil {
t.Fatal(err)
} else if room.Room.RoomId != roomId {
t.Fatalf("Expected room %s, got %s", roomId, room.Room.RoomId)
}
// We will receive a "joined" event.
if err := client1.RunUntilJoined(ctx, hello1.Hello); err != nil {
t.Error(err)
}
// Join room by id (second client).
if room, err := client2.JoinRoom(ctx, roomId); err != nil {
t.Fatal(err)
} else if room.Room.RoomId != roomId {
t.Fatalf("Expected room %s, got %s", roomId, room.Room.RoomId)
}
// We will receive a "joined" event for the first and the second client.
if events, unexpected, err := client2.RunUntilJoinedAndReturn(ctx, hello1.Hello, hello2.Hello); err != nil {
t.Error(err)
} else {
if len(unexpected) > 0 {
t.Errorf("Received unexpected messages: %+v", unexpected)
} else if len(events) != 2 {
t.Errorf("Expected two event, got %+v", events)
} else if events[0].User != nil {
t.Errorf("Expected empty userdata for first event, got %+v", events[0].User)
} else if events[1].User != nil {
t.Errorf("Expected empty userdata for second event, got %+v", events[1].User)
}
}
// The first client will also receive a "joined" event from the second client.
if events, unexpected, err := client1.RunUntilJoinedAndReturn(ctx, hello2.Hello); err != nil {
t.Error(err)
} else {
if len(unexpected) > 0 {
t.Errorf("Received unexpected messages: %+v", unexpected)
} else if len(events) != 1 {
t.Errorf("Expected one event, got %+v", events)
} else if events[0].User == nil {
t.Errorf("Expected userdata for first event, got nothing")
}
}
}
func TestJoinRoomSwitchClient(t *testing.T) {
hub, _, _, server := CreateHubForTest(t)

View File

@ -36,6 +36,14 @@ var (
PERMISSION_MAY_PUBLISH_SCREEN Permission = "publish-screen"
PERMISSION_MAY_CONTROL Permission = "control"
PERMISSION_TRANSIENT_DATA Permission = "transient-data"
PERMISSION_HIDE_DISPLAYNAMES Permission = "hide-displaynames"
// DefaultPermissionOverrides contains permission overrides for users where
// no permissions have been set by the server. If a permission is not set in
// this map, it's assumed the user has that permission.
DefaultPermissionOverrides = map[Permission]bool{
PERMISSION_HIDE_DISPLAYNAMES: false,
}
)
type SessionIdData struct {