From 8827db7a3a07b79180de4e881e4bb7788c0a4381 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20Monnom?= Date: Fri, 23 Jan 2026 09:21:16 -0800 Subject: [PATCH] wip --- auth/grants.go | 47 +++++++++++++++++++++++---- auth/grants_test.go | 79 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 120 insertions(+), 6 deletions(-) diff --git a/auth/grants.go b/auth/grants.go index 4743ef8bd..ca9883072 100644 --- a/auth/grants.go +++ b/auth/grants.go @@ -553,24 +553,59 @@ func (s *SIPGrant) MarshalLogObject(e zapcore.ObjectEncoder) error { type AgentGrant struct { // Admin grants to create/update/delete Cloud Agents. Admin bool `json:"admin,omitempty"` + + // Endpoints specifies which agent endpoints the holder can use. + // When empty, no endpoint access is granted. + // Use "*" to grant access to all endpoints. + Endpoints []string `json:"endpoints,omitempty"` } -func (s *AgentGrant) Clone() *AgentGrant { - if s == nil { +func (a *AgentGrant) SetEndpoints(endpoints []string) { + a.Endpoints = endpoints +} + +func (a *AgentGrant) GetEndpoints() []string { + return a.Endpoints +} + +// CanUseEndpoint returns true if the grant allows using the specified endpoint. +// The endpoint must be explicitly listed in Endpoints, or Endpoints must contain +// "*" for wildcard access. Note: Admin is a separate permission for Cloud Agents +// CRUD operations and does not grant endpoint access. +func (a *AgentGrant) CanUseEndpoint(endpoint string) bool { + if a == nil { + return false + } + for _, e := range a.Endpoints { + if e == "*" || e == endpoint { + return true + } + } + return false +} + +func (a *AgentGrant) Clone() *AgentGrant { + if a == nil { return nil } - clone := *s + clone := *a + + if a.Endpoints != nil { + clone.Endpoints = make([]string, len(a.Endpoints)) + copy(clone.Endpoints, a.Endpoints) + } return &clone } -func (s *AgentGrant) MarshalLogObject(e zapcore.ObjectEncoder) error { - if s == nil { +func (a *AgentGrant) MarshalLogObject(e zapcore.ObjectEncoder) error { + if a == nil { return nil } - e.AddBool("Admin", s.Admin) + e.AddBool("Admin", a.Admin) + e.AddArray("Endpoints", logger.StringSlice(a.Endpoints)) return nil } diff --git a/auth/grants_test.go b/auth/grants_test.go index 211bc2097..51267b298 100644 --- a/auth/grants_test.go +++ b/auth/grants_test.go @@ -138,6 +138,27 @@ func TestGrants(t *testing.T) { require.True(t, reflect.DeepEqual(grants.Agent, clone.Agent)) }) + t.Run("clone with Agent endpoints", func(t *testing.T) { + agent := &AgentGrant{ + Admin: false, + Endpoints: []string{"customer-service", "my-agent"}, + } + grants := &ClaimGrants{ + Identity: "identity", + Agent: agent, + } + clone := grants.Clone() + require.NotSame(t, grants, clone) + require.NotSame(t, grants.Agent, clone.Agent) + require.NotSame(t, &grants.Agent.Endpoints, &clone.Agent.Endpoints) + require.Equal(t, grants.Agent.Endpoints, clone.Agent.Endpoints) + require.True(t, reflect.DeepEqual(grants, clone)) + + // Modifying clone should not affect original + clone.Agent.Endpoints[0] = "modified" + require.NotEqual(t, grants.Agent.Endpoints[0], clone.Agent.Endpoints[0]) + }) + t.Run("clone with Inference", func(t *testing.T) { inference := &InferenceGrant{ Perform: true, @@ -159,6 +180,64 @@ func TestGrants(t *testing.T) { }) } +func TestAgentGrantCanUseEndpoint(t *testing.T) { + t.Parallel() + + t.Run("nil grant returns false", func(t *testing.T) { + var agent *AgentGrant + require.False(t, agent.CanUseEndpoint("any-endpoint")) + }) + + t.Run("admin alone does not grant endpoint access", func(t *testing.T) { + agent := &AgentGrant{Admin: true} + require.False(t, agent.CanUseEndpoint("customer-service")) + require.False(t, agent.CanUseEndpoint("my-agent")) + }) + + t.Run("empty endpoints denies access", func(t *testing.T) { + agent := &AgentGrant{Admin: false} + require.False(t, agent.CanUseEndpoint("customer-service")) + }) + + t.Run("explicit endpoint grants access", func(t *testing.T) { + agent := &AgentGrant{ + Admin: false, + Endpoints: []string{"customer-service", "my-agent"}, + } + require.True(t, agent.CanUseEndpoint("customer-service")) + require.True(t, agent.CanUseEndpoint("my-agent")) + require.False(t, agent.CanUseEndpoint("other-agent")) + }) + + t.Run("wildcard grants access to all", func(t *testing.T) { + agent := &AgentGrant{ + Admin: false, + Endpoints: []string{"*"}, + } + require.True(t, agent.CanUseEndpoint("customer-service")) + require.True(t, agent.CanUseEndpoint("my-agent")) + require.True(t, agent.CanUseEndpoint("any-endpoint")) + }) + + t.Run("wildcard with other endpoints", func(t *testing.T) { + agent := &AgentGrant{ + Admin: false, + Endpoints: []string{"specific-agent", "*"}, + } + require.True(t, agent.CanUseEndpoint("specific-agent")) + require.True(t, agent.CanUseEndpoint("any-other-agent")) + }) + + t.Run("admin with endpoints grants endpoint access", func(t *testing.T) { + agent := &AgentGrant{ + Admin: true, + Endpoints: []string{"customer-service"}, + } + require.True(t, agent.CanUseEndpoint("customer-service")) + require.False(t, agent.CanUseEndpoint("other-agent")) + }) +} + func TestParticipantKind(t *testing.T) { const kindMin, kindMax = livekit.ParticipantInfo_STANDARD, livekit.ParticipantInfo_AGENT for k := kindMin; k <= kindMax; k++ {