diff --git a/.changeset/short-deer-eat.md b/.changeset/short-deer-eat.md new file mode 100644 index 000000000..8f09f1fbe --- /dev/null +++ b/.changeset/short-deer-eat.md @@ -0,0 +1,5 @@ +--- +"github.com/livekit/protocol": patch +--- + +support parsing rtc as grant in addition to video diff --git a/auth/grants.go b/auth/grants.go index 1073073eb..3a2a1ff5a 100644 --- a/auth/grants.go +++ b/auth/grants.go @@ -15,6 +15,7 @@ package auth import ( + "encoding/json" "errors" "maps" "strings" @@ -245,6 +246,58 @@ func (c *ClaimGrants) MarshalLogObject(e zapcore.ObjectEncoder) error { return nil } +// claimGrantsJSON is used for JSON marshaling/unmarshaling to support both +// "video" (legacy) and "rtc" (new) field names for VideoGrant. +// This enables backwards compatibility during the migration from VideoGrant to RTCGrant. +type claimGrantsJSON struct { + Identity string `json:"identity,omitempty"` + Name string `json:"name,omitempty"` + Kind string `json:"kind,omitempty"` + KindDetails []string `json:"kindDetails,omitempty"` + Video *VideoGrant `json:"video,omitempty"` + RTC *VideoGrant `json:"rtc,omitempty"` + SIP *SIPGrant `json:"sip,omitempty"` + Agent *AgentGrant `json:"agent,omitempty"` + Inference *InferenceGrant `json:"inference,omitempty"` + Observability *ObservabilityGrant `json:"observability,omitempty"` + RoomConfig *RoomConfiguration `json:"roomConfig,omitempty"` + RoomPreset string `json:"roomPreset,omitempty"` + Sha256 string `json:"sha256,omitempty"` + Metadata string `json:"metadata,omitempty"` + Attributes map[string]string `json:"attributes,omitempty"` +} + +func (c *ClaimGrants) UnmarshalJSON(data []byte) error { + var j claimGrantsJSON + if err := json.Unmarshal(data, &j); err != nil { + return err + } + + c.Identity = j.Identity + c.Name = j.Name + c.Kind = j.Kind + c.KindDetails = j.KindDetails + c.SIP = j.SIP + c.Agent = j.Agent + c.Inference = j.Inference + c.Observability = j.Observability + c.RoomConfig = j.RoomConfig + c.RoomPreset = j.RoomPreset + c.Sha256 = j.Sha256 + c.Metadata = j.Metadata + c.Attributes = j.Attributes + + // Support both "video" (legacy) and "rtc" (new) field names. + // If both are present, "rtc" takes precedence. + if j.RTC != nil { + c.Video = j.RTC + } else { + c.Video = j.Video + } + + return nil +} + // ------------------------------------------------------------- type VideoGrant struct { diff --git a/auth/grants_test.go b/auth/grants_test.go index 211bc2097..459805a2e 100644 --- a/auth/grants_test.go +++ b/auth/grants_test.go @@ -15,6 +15,7 @@ package auth import ( + "encoding/json" "reflect" "strconv" "testing" @@ -159,6 +160,120 @@ func TestGrants(t *testing.T) { }) } +func TestClaimGrantsVideoRTCCompat(t *testing.T) { + t.Parallel() + + t.Run("unmarshal with video field", func(t *testing.T) { + jsonData := `{"identity":"user1","video":{"roomJoin":true,"room":"test-room","canPublish":true}}` + + var grants ClaimGrants + err := json.Unmarshal([]byte(jsonData), &grants) + require.NoError(t, err) + require.Equal(t, "user1", grants.Identity) + require.NotNil(t, grants.Video) + require.True(t, grants.Video.RoomJoin) + require.Equal(t, "test-room", grants.Video.Room) + require.True(t, grants.Video.GetCanPublish()) + }) + + t.Run("unmarshal with rtc field", func(t *testing.T) { + jsonData := `{"identity":"user2","rtc":{"roomJoin":true,"room":"rtc-room","canSubscribe":false}}` + + var grants ClaimGrants + err := json.Unmarshal([]byte(jsonData), &grants) + require.NoError(t, err) + require.Equal(t, "user2", grants.Identity) + require.NotNil(t, grants.Video) + require.True(t, grants.Video.RoomJoin) + require.Equal(t, "rtc-room", grants.Video.Room) + require.False(t, grants.Video.GetCanSubscribe()) + }) + + t.Run("unmarshal with both video and rtc fields prefers rtc", func(t *testing.T) { + jsonData := `{"identity":"user3","video":{"room":"video-room"},"rtc":{"room":"rtc-room"}}` + + var grants ClaimGrants + err := json.Unmarshal([]byte(jsonData), &grants) + require.NoError(t, err) + require.NotNil(t, grants.Video) + require.Equal(t, "rtc-room", grants.Video.Room, "rtc field should take precedence over video") + }) + + t.Run("marshal outputs video field", func(t *testing.T) { + grants := &ClaimGrants{ + Identity: "user4", + Video: &VideoGrant{ + RoomJoin: true, + Room: "my-room", + }, + } + + data, err := json.Marshal(grants) + require.NoError(t, err) + + // Verify the output contains "video" not "rtc" + var rawMap map[string]interface{} + err = json.Unmarshal(data, &rawMap) + require.NoError(t, err) + require.Contains(t, rawMap, "video", "marshaled JSON should contain 'video' field") + require.NotContains(t, rawMap, "rtc", "marshaled JSON should not contain 'rtc' field") + }) + + t.Run("roundtrip with video field preserves data", func(t *testing.T) { + original := &ClaimGrants{ + Identity: "user5", + Name: "Test User", + Video: &VideoGrant{ + RoomJoin: true, + Room: "test-room", + RoomCreate: true, + }, + SIP: &SIPGrant{Admin: true}, + } + + data, err := json.Marshal(original) + require.NoError(t, err) + + var decoded ClaimGrants + err = json.Unmarshal(data, &decoded) + require.NoError(t, err) + + require.Equal(t, original.Identity, decoded.Identity) + require.Equal(t, original.Name, decoded.Name) + require.Equal(t, original.Video.RoomJoin, decoded.Video.RoomJoin) + require.Equal(t, original.Video.Room, decoded.Video.Room) + require.Equal(t, original.Video.RoomCreate, decoded.Video.RoomCreate) + require.Equal(t, original.SIP.Admin, decoded.SIP.Admin) + }) + + t.Run("unmarshal with all grant types via rtc", func(t *testing.T) { + jsonData := `{ + "identity": "agent1", + "kind": "agent", + "rtc": {"roomJoin": true, "room": "agent-room", "agent": true}, + "sip": {"admin": true}, + "agent": {"admin": true}, + "inference": {"perform": true} + }` + + var grants ClaimGrants + err := json.Unmarshal([]byte(jsonData), &grants) + require.NoError(t, err) + require.Equal(t, "agent1", grants.Identity) + require.Equal(t, "agent", grants.Kind) + require.NotNil(t, grants.Video) + require.True(t, grants.Video.RoomJoin) + require.Equal(t, "agent-room", grants.Video.Room) + require.True(t, grants.Video.Agent) + require.NotNil(t, grants.SIP) + require.True(t, grants.SIP.Admin) + require.NotNil(t, grants.Agent) + require.True(t, grants.Agent.Admin) + require.NotNil(t, grants.Inference) + require.True(t, grants.Inference.Perform) + }) +} + func TestParticipantKind(t *testing.T) { const kindMin, kindMax = livekit.ParticipantInfo_STANDARD, livekit.ParticipantInfo_AGENT for k := kindMin; k <= kindMax; k++ {