From 375c22c5b09bce1196a7c1cdfc22c63b997aa204 Mon Sep 17 00:00:00 2001 From: Mihai Doarna Date: Thu, 11 Dec 2025 17:52:03 +0200 Subject: [PATCH] add the missing fields to the team search results --- apps/iam/pkg/apis/iam/v0alpha1/team_search.go | 15 ++++++--- pkg/registry/apis/iam/team/legacy_search.go | 22 +++++++++++-- .../apis/iam/team/legacy_search_test.go | 11 +++++-- pkg/registry/apis/iam/team_search.go | 3 ++ pkg/registry/apis/iam/team_search_test.go | 2 +- pkg/services/team/search/search.go | 31 +++++++++++++++++++ pkg/services/team/search/search_test.go | 28 +++++++++++++++++ .../unified/search/builders/team_search.go | 24 ++++++++++++-- 8 files changed, 123 insertions(+), 13 deletions(-) diff --git a/apps/iam/pkg/apis/iam/v0alpha1/team_search.go b/apps/iam/pkg/apis/iam/v0alpha1/team_search.go index 545552a4795..1a1c8095fac 100644 --- a/apps/iam/pkg/apis/iam/v0alpha1/team_search.go +++ b/apps/iam/pkg/apis/iam/v0alpha1/team_search.go @@ -2,6 +2,8 @@ package v0alpha1 import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/grafana/grafana/pkg/services/team" ) // +k8s:deepcopy-gen=true @@ -27,9 +29,12 @@ type TeamSearchResults struct { // +k8s:deepcopy-gen=true type TeamHit struct { - Name string `json:"name"` - Title string `json:"title"` - Email string `json:"email,omitempty"` - Provisioned bool `json:"provisioned,omitempty"` - ExternalUID string `json:"externalUID,omitempty"` + Name string `json:"name"` + Title string `json:"title"` + Email string `json:"email,omitempty"` + Provisioned bool `json:"provisioned,omitempty"` + ExternalUID string `json:"externalUID,omitempty"` + MemberCount int64 `json:"memberCount"` + Permission team.PermissionType `json:"permission"` + AccessControl map[string]bool `json:"accessControl,omitempty"` } diff --git a/pkg/registry/apis/iam/team/legacy_search.go b/pkg/registry/apis/iam/team/legacy_search.go index 938062bd9e6..beb4930c068 100644 --- a/pkg/registry/apis/iam/team/legacy_search.go +++ b/pkg/registry/apis/iam/team/legacy_search.go @@ -2,6 +2,8 @@ package team import ( "context" + "encoding/binary" + "encoding/json" "fmt" "log/slog" "math" @@ -77,7 +79,7 @@ func (c *LegacyTeamSearchClient) Search(ctx context.Context, req *resourcepb.Res namespace := signedInUser.GetNamespace() for _, t := range res.Teams { - cells := createCells(t, req.Fields) + cells := createCells(c.log, t, req.Fields) list.Results.Rows = append(list.Results.Rows, &resourcepb.ResourceTableRow{ Key: getResourceKey(t, namespace), Cells: cells, @@ -118,7 +120,7 @@ func getDefaultColumns() []*resourcepb.ResourceTableColumnDefinition { } } -func createCells(t *team.TeamDTO, fields []string) [][]byte { +func createCells(log *slog.Logger, t *team.TeamDTO, fields []string) [][]byte { cells := createDefaultCells(t) for _, field := range fields { switch field { @@ -128,6 +130,22 @@ func createCells(t *team.TeamDTO, fields []string) [][]byte { cells = append(cells, []byte(strconv.FormatBool(t.IsProvisioned))) case builders.TEAM_SEARCH_EXTERNAL_UID: cells = append(cells, []byte(t.ExternalUID)) + case builders.TEAM_SEARCH_MEMBER_COUNT: + b := make([]byte, 8) + binary.BigEndian.PutUint64(b, uint64(t.MemberCount)) + cells = append(cells, b) + case builders.TEAM_SEARCH_PERMISSION: + b := make([]byte, 4) + binary.BigEndian.PutUint32(b, uint32(t.Permission)) + cells = append(cells, b) + case builders.TEAM_SEARCH_ACCESS_CONTROL: + accessControl, err := json.Marshal(t.AccessControl) + if err != nil { + log.Error("error marshalling access control", "error", err) + cells = append(cells, []byte("")) + continue + } + cells = append(cells, accessControl) } } return cells diff --git a/pkg/registry/apis/iam/team/legacy_search_test.go b/pkg/registry/apis/iam/team/legacy_search_test.go index f7d70f355c3..50e4fc67754 100644 --- a/pkg/registry/apis/iam/team/legacy_search_test.go +++ b/pkg/registry/apis/iam/team/legacy_search_test.go @@ -2,6 +2,7 @@ package team import ( "context" + "encoding/binary" "errors" "math" "testing" @@ -25,7 +26,7 @@ func TestLegacyTeamSearchClient_Search(t *testing.T) { Limit: 10, Page: 1, Query: "test", - Fields: []string{"name", "email", "provisioned", "externalUID"}, + Fields: []string{"name", "email", "provisioned", "externalUID", "memberCount", "permission", "accessControl"}, } mockTeamService.ExpectedSearchTeamsResult = team.SearchTeamQueryResult{ @@ -36,6 +37,9 @@ func TestLegacyTeamSearchClient_Search(t *testing.T) { Email: "test@example.com", IsProvisioned: true, ExternalUID: "testExternalUID", + MemberCount: 10, + Permission: team.PermissionTypeAdmin, + AccessControl: map[string]bool{"test": true}, }, }, TotalCount: 1, @@ -48,7 +52,7 @@ func TestLegacyTeamSearchClient_Search(t *testing.T) { require.NoError(t, err) require.Equal(t, int64(1), resp.TotalHits) require.Len(t, resp.Results.Rows, 1) - require.Len(t, resp.Results.Columns, 5) + require.Len(t, resp.Results.Columns, 8) require.Equal(t, "default", resp.Results.Rows[0].Key.Namespace) require.Equal(t, "iam.grafana.com", resp.Results.Rows[0].Key.Group) require.Equal(t, "teams", resp.Results.Rows[0].Key.Resource) @@ -58,6 +62,9 @@ func TestLegacyTeamSearchClient_Search(t *testing.T) { require.Equal(t, "test@example.com", string(resp.Results.Rows[0].Cells[2])) require.Equal(t, "true", string(resp.Results.Rows[0].Cells[3])) require.Equal(t, "testExternalUID", string(resp.Results.Rows[0].Cells[4])) + require.Equal(t, 10, int(binary.BigEndian.Uint64(resp.Results.Rows[0].Cells[5]))) + require.Equal(t, team.PermissionTypeAdmin, team.PermissionType(binary.BigEndian.Uint32(resp.Results.Rows[0].Cells[6]))) + require.Equal(t, "{\"test\":true}", string(resp.Results.Rows[0].Cells[7])) }) t.Run("returns error if page is negative", func(t *testing.T) { diff --git a/pkg/registry/apis/iam/team_search.go b/pkg/registry/apis/iam/team_search.go index 786bf9be834..4abfa542822 100644 --- a/pkg/registry/apis/iam/team_search.go +++ b/pkg/registry/apis/iam/team_search.go @@ -164,6 +164,9 @@ func (s *TeamSearchHandler) DoTeamSearch(w http.ResponseWriter, r *http.Request) builders.TEAM_SEARCH_EMAIL, builders.TEAM_SEARCH_PROVISIONED, builders.TEAM_SEARCH_EXTERNAL_UID, + builders.TEAM_SEARCH_MEMBER_COUNT, + builders.TEAM_SEARCH_PERMISSION, + builders.TEAM_SEARCH_ACCESS_CONTROL, }, } diff --git a/pkg/registry/apis/iam/team_search_test.go b/pkg/registry/apis/iam/team_search_test.go index 76efed1c067..c0de46f2698 100644 --- a/pkg/registry/apis/iam/team_search_test.go +++ b/pkg/registry/apis/iam/team_search_test.go @@ -89,7 +89,7 @@ func TestTeamSearchHandler(t *testing.T) { if mockClient.LastSearchRequest == nil { t.Fatalf("expected Search to be called, but it was not") } - expectedFields := []string{"email", "provisioned", "externalUID"} + expectedFields := []string{"email", "provisioned", "externalUID", "memberCount", "permission", "accessControl"} if fmt.Sprintf("%v", mockClient.LastSearchRequest.Fields) != fmt.Sprintf("%v", expectedFields) { t.Errorf("expected fields %v, got %v", expectedFields, mockClient.LastSearchRequest.Fields) } diff --git a/pkg/services/team/search/search.go b/pkg/services/team/search/search.go index 51c6113add3..1a3b1a3ba23 100644 --- a/pkg/services/team/search/search.go +++ b/pkg/services/team/search/search.go @@ -1,9 +1,12 @@ package search import ( + "encoding/binary" + "encoding/json" "fmt" "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1" + "github.com/grafana/grafana/pkg/services/team" "github.com/grafana/grafana/pkg/storage/unified/resource" "github.com/grafana/grafana/pkg/storage/unified/resourcepb" "github.com/grafana/grafana/pkg/storage/unified/search/builders" @@ -22,6 +25,9 @@ func ParseResults(result *resourcepb.ResourceSearchResponse, offset int64) (v0al emailIDX := -1 provisionedIDX := -1 externalUIDIDX := -1 + memberCountIDX := -1 + permissionIDX := -1 + accessControlIDX := -1 for i, v := range result.Results.Columns { if v == nil { @@ -37,6 +43,12 @@ func ParseResults(result *resourcepb.ResourceSearchResponse, offset int64) (v0al provisionedIDX = i case builders.TEAM_SEARCH_EXTERNAL_UID: externalUIDIDX = i + case builders.TEAM_SEARCH_MEMBER_COUNT: + memberCountIDX = i + case builders.TEAM_SEARCH_PERMISSION: + permissionIDX = i + case builders.TEAM_SEARCH_ACCESS_CONTROL: + accessControlIDX = i } } @@ -75,6 +87,25 @@ func ParseResults(result *resourcepb.ResourceSearchResponse, offset int64) (v0al hit.ExternalUID = string(row.Cells[externalUIDIDX]) } + if memberCountIDX >= 0 && row.Cells[memberCountIDX] != nil { + memberCount := binary.BigEndian.Uint64(row.Cells[memberCountIDX]) + hit.MemberCount = int64(memberCount) + } + + if permissionIDX >= 0 && row.Cells[permissionIDX] != nil { + permission := binary.BigEndian.Uint32(row.Cells[permissionIDX]) + hit.Permission = team.PermissionType(permission) + } + + if accessControlIDX >= 0 && row.Cells[accessControlIDX] != nil { + var accessControl map[string]bool + err := json.Unmarshal(row.Cells[accessControlIDX], &accessControl) + if err != nil { + return v0alpha1.TeamSearchResults{}, fmt.Errorf("error parsing team search response: error unmarshalling access control: %w", err) + } + hit.AccessControl = accessControl + } + sr.Hits[i] = *hit } diff --git a/pkg/services/team/search/search_test.go b/pkg/services/team/search/search_test.go index 2dc5c6ef588..d9a4da0e756 100644 --- a/pkg/services/team/search/search_test.go +++ b/pkg/services/team/search/search_test.go @@ -1,10 +1,12 @@ package search import ( + "encoding/binary" "testing" "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/services/team" "github.com/grafana/grafana/pkg/storage/unified/resourcepb" ) @@ -29,6 +31,18 @@ func TestParseResults(t *testing.T) { Name: "externalUID", Type: resourcepb.ResourceTableColumnDefinition_STRING, }, + { + Name: "memberCount", + Type: resourcepb.ResourceTableColumnDefinition_INT64, + }, + { + Name: "permission", + Type: resourcepb.ResourceTableColumnDefinition_INT32, + }, + { + Name: "accessControl", + Type: resourcepb.ResourceTableColumnDefinition_OBJECT, + }, }, Rows: []*resourcepb.ResourceTableRow{ { @@ -41,6 +55,17 @@ func TestParseResults(t *testing.T) { []byte("team1@example.com"), []byte("true"), []byte("team1-uid"), + func() []byte { + b := make([]byte, 8) + binary.BigEndian.PutUint64(b, 10) + return b + }(), + func() []byte { + b := make([]byte, 4) + binary.BigEndian.PutUint32(b, 4) + return b + }(), + []byte("{\"test\":true}"), }, }, }, @@ -55,6 +80,9 @@ func TestParseResults(t *testing.T) { require.Equal(t, "team1@example.com", results.Hits[0].Email) require.True(t, results.Hits[0].Provisioned) require.Equal(t, "team1-uid", results.Hits[0].ExternalUID) + require.Equal(t, int64(10), results.Hits[0].MemberCount) + require.Equal(t, team.PermissionTypeAdmin, results.Hits[0].Permission) + require.Equal(t, map[string]bool{"test": true}, results.Hits[0].AccessControl) }) t.Run("should handle nil result", func(t *testing.T) { diff --git a/pkg/storage/unified/search/builders/team_search.go b/pkg/storage/unified/search/builders/team_search.go index 4b09074b3dc..c631419c56b 100644 --- a/pkg/storage/unified/search/builders/team_search.go +++ b/pkg/storage/unified/search/builders/team_search.go @@ -14,9 +14,12 @@ import ( ) const ( - TEAM_SEARCH_EMAIL = "email" - TEAM_SEARCH_PROVISIONED = "provisioned" - TEAM_SEARCH_EXTERNAL_UID = "externalUID" + TEAM_SEARCH_EMAIL = "email" + TEAM_SEARCH_PROVISIONED = "provisioned" + TEAM_SEARCH_EXTERNAL_UID = "externalUID" + TEAM_SEARCH_MEMBER_COUNT = "memberCount" + TEAM_SEARCH_PERMISSION = "permission" + TEAM_SEARCH_ACCESS_CONTROL = "accessControl" ) var TeamSearchTableColumnDefinitions = map[string]*resourcepb.ResourceTableColumnDefinition{ @@ -35,6 +38,21 @@ var TeamSearchTableColumnDefinitions = map[string]*resourcepb.ResourceTableColum Type: resourcepb.ResourceTableColumnDefinition_STRING, Description: "External UID of the team", }, + TEAM_SEARCH_MEMBER_COUNT: { + Name: TEAM_SEARCH_MEMBER_COUNT, + Type: resourcepb.ResourceTableColumnDefinition_INT64, + Description: "Number of members in the team", + }, + TEAM_SEARCH_PERMISSION: { + Name: TEAM_SEARCH_PERMISSION, + Type: resourcepb.ResourceTableColumnDefinition_INT32, + Description: "Permission of the team", + }, + TEAM_SEARCH_ACCESS_CONTROL: { + Name: TEAM_SEARCH_ACCESS_CONTROL, + Type: resourcepb.ResourceTableColumnDefinition_OBJECT, + Description: "Access control of the team", + }, } func GetTeamSearchBuilder() (resource.DocumentBuilderInfo, error) {