IAM: Improvements on the user teams endpoint (#117082)

minor changes on the user teams endpoint
This commit is contained in:
Mihai Doarna 2026-02-03 10:46:36 +02:00 committed by GitHub
parent 57958926ab
commit 56bde41a99
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 132 additions and 132 deletions

View file

@ -38,8 +38,9 @@ userv0alpha1: userKind & {
"GET": {
response: {
#UserTeam: {
teamRef: v0alpha1.TeamRef
permission: v0alpha1.TeamPermission
user: string
team: string
permission: string
external: bool
}
items: [...#UserTeam]

View file

@ -4,37 +4,17 @@ package v0alpha1
// +k8s:openapi-gen=true
type VersionsV0alpha1Kinds6RoutesTeamsGETResponseUserTeam struct {
TeamRef TeamRef `json:"teamRef"`
Permission TeamPermission `json:"permission"`
External bool `json:"external"`
User string `json:"user"`
Team string `json:"team"`
Permission string `json:"permission"`
External bool `json:"external"`
}
// NewVersionsV0alpha1Kinds6RoutesTeamsGETResponseUserTeam creates a new VersionsV0alpha1Kinds6RoutesTeamsGETResponseUserTeam object.
func NewVersionsV0alpha1Kinds6RoutesTeamsGETResponseUserTeam() *VersionsV0alpha1Kinds6RoutesTeamsGETResponseUserTeam {
return &VersionsV0alpha1Kinds6RoutesTeamsGETResponseUserTeam{
TeamRef: *NewTeamRef(),
}
return &VersionsV0alpha1Kinds6RoutesTeamsGETResponseUserTeam{}
}
// +k8s:openapi-gen=true
type TeamRef struct {
// Name is the unique identifier for a team.
Name string `json:"name"`
}
// NewTeamRef creates a new TeamRef object.
func NewTeamRef() *TeamRef {
return &TeamRef{}
}
// +k8s:openapi-gen=true
type TeamPermission string
const (
TeamPermissionAdmin TeamPermission = "admin"
TeamPermissionMember TeamPermission = "member"
)
// +k8s:openapi-gen=true
type GetTeamsBody struct {
Items []VersionsV0alpha1Kinds6RoutesTeamsGETResponseUserTeam `json:"items"`

View file

@ -76,7 +76,6 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamLBACRuleList": schema_pkg_apis_iam_v0alpha1_TeamLBACRuleList(ref),
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamLBACRuleSpec": schema_pkg_apis_iam_v0alpha1_TeamLBACRuleSpec(ref),
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamList": schema_pkg_apis_iam_v0alpha1_TeamList(ref),
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamRef": schema_pkg_apis_iam_v0alpha1_TeamRef(ref),
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamSpec": schema_pkg_apis_iam_v0alpha1_TeamSpec(ref),
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamStatus": schema_pkg_apis_iam_v0alpha1_TeamStatus(ref),
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamstatusOperatorState": schema_pkg_apis_iam_v0alpha1_TeamstatusOperatorState(ref),
@ -3013,27 +3012,6 @@ func schema_pkg_apis_iam_v0alpha1_TeamList(ref common.ReferenceCallback) common.
}
}
func schema_pkg_apis_iam_v0alpha1_TeamRef(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
Properties: map[string]spec.Schema{
"name": {
SchemaProps: spec.SchemaProps{
Description: "Name is the unique identifier for a team.",
Default: "",
Type: []string{"string"},
Format: "",
},
},
},
Required: []string{"name"},
},
},
}
}
func schema_pkg_apis_iam_v0alpha1_TeamSpec(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
@ -3437,10 +3415,18 @@ func schema_pkg_apis_iam_v0alpha1_VersionsV0alpha1Kinds6RoutesTeamsGETResponseUs
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
Properties: map[string]spec.Schema{
"teamRef": {
"user": {
SchemaProps: spec.SchemaProps{
Default: map[string]interface{}{},
Ref: ref("github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamRef"),
Default: "",
Type: []string{"string"},
Format: "",
},
},
"team": {
SchemaProps: spec.SchemaProps{
Default: "",
Type: []string{"string"},
Format: "",
},
},
"permission": {
@ -3458,11 +3444,9 @@ func schema_pkg_apis_iam_v0alpha1_VersionsV0alpha1Kinds6RoutesTeamsGETResponseUs
},
},
},
Required: []string{"teamRef", "permission", "external"},
Required: []string{"user", "team", "permission", "external"},
},
},
Dependencies: []string{
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamRef"},
}
}

View file

@ -484,7 +484,7 @@ func (b *IdentityAccessManagementAPIBuilder) UpdateUsersAPIGroup(opts builder.AP
b.features,
)
storage[userResource.StoragePath("teams")] = user.NewTeamMemberREST(teamBindingSearchClient, b.tracing, b.features)
storage[userResource.StoragePath("teams")] = user.NewUserTeamREST(teamBindingSearchClient, b.tracing, b.features)
return nil
}

View file

@ -36,7 +36,7 @@ type UserTeamREST struct {
features featuremgmt.FeatureToggles
}
func NewTeamMemberREST(client resourcepb.ResourceIndexClient, tracer trace.Tracer, features featuremgmt.FeatureToggles) *UserTeamREST {
func NewUserTeamREST(client resourcepb.ResourceIndexClient, tracer trace.Tracer, features featuremgmt.FeatureToggles) *UserTeamREST {
return &UserTeamREST{
log: log.New("grafana-apiserver.user.teams"),
client: client,
@ -123,6 +123,7 @@ func (s *UserTeamREST) Connect(ctx context.Context, name string, options runtime
Page: int64(page),
Explain: queryParams.Has("explain") && queryParams.Get("explain") != "false",
Fields: []string{
resource.SEARCH_FIELD_PREFIX + builders.TEAM_BINDING_SUBJECT_NAME,
resource.SEARCH_FIELD_PREFIX + builders.TEAM_BINDING_TEAM_REF,
resource.SEARCH_FIELD_PREFIX + builders.TEAM_BINDING_PERMISSION,
resource.SEARCH_FIELD_PREFIX + builders.TEAM_BINDING_EXTERNAL,
@ -135,7 +136,7 @@ func (s *UserTeamREST) Connect(ctx context.Context, name string, options runtime
return
}
searchResults, err := s.parseResults(result, searchRequest.Offset)
searchResults, err := parseResults(result, searchRequest.Offset)
if err != nil {
responder.Error(err)
return
@ -158,7 +159,7 @@ func (s *UserTeamREST) ConnectMethods() []string {
return []string{http.MethodGet}
}
func (h *UserTeamREST) parseResults(result *resourcepb.ResourceSearchResponse, offset int64) (iamv0alpha1.GetTeamsBody, error) {
func parseResults(result *resourcepb.ResourceSearchResponse, offset int64) (iamv0alpha1.GetTeamsBody, error) {
if result == nil {
return iamv0alpha1.GetTeamsBody{}, nil
}
@ -169,7 +170,8 @@ func (h *UserTeamREST) parseResults(result *resourcepb.ResourceSearchResponse, o
return iamv0alpha1.GetTeamsBody{}, nil
}
teamRefIDX := -1
userIDX := -1
teamIDX := -1
permissionIDX := -1
externalIDX := -1
@ -179,8 +181,10 @@ func (h *UserTeamREST) parseResults(result *resourcepb.ResourceSearchResponse, o
}
switch v.Name {
case builders.TEAM_BINDING_SUBJECT_NAME:
userIDX = i
case builders.TEAM_BINDING_TEAM_REF:
teamRefIDX = i
teamIDX = i
case builders.TEAM_BINDING_PERMISSION:
permissionIDX = i
case builders.TEAM_BINDING_EXTERNAL:
@ -188,7 +192,10 @@ func (h *UserTeamREST) parseResults(result *resourcepb.ResourceSearchResponse, o
}
}
if teamRefIDX < 0 {
if userIDX < 0 {
return iamv0alpha1.GetTeamsBody{}, fmt.Errorf("required column '%s' not found in search results", builders.TEAM_BINDING_SUBJECT_NAME)
}
if teamIDX < 0 {
return iamv0alpha1.GetTeamsBody{}, fmt.Errorf("required column '%s' not found in search results", builders.TEAM_BINDING_TEAM_REF)
}
if permissionIDX < 0 {
@ -208,8 +215,9 @@ func (h *UserTeamREST) parseResults(result *resourcepb.ResourceSearchResponse, o
}
body.Items[i] = iamv0alpha1.VersionsV0alpha1Kinds6RoutesTeamsGETResponseUserTeam{
TeamRef: iamv0alpha1.TeamRef{Name: string(row.Cells[teamRefIDX])},
Permission: iamv0alpha1.TeamPermission(string(row.Cells[permissionIDX])),
User: string(row.Cells[userIDX]),
Team: string(row.Cells[teamIDX]),
Permission: string(row.Cells[permissionIDX]),
External: string(row.Cells[externalIDX]) == "true",
}
}

View file

@ -9,7 +9,6 @@ import (
"testing"
"github.com/stretchr/testify/require"
"go.opentelemetry.io/otel/trace/noop"
"k8s.io/apimachinery/pkg/runtime"
iamv0alpha1 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1"
@ -41,7 +40,7 @@ func (m *mockResponder) Error(err error) {
func TestUserTeamREST_Connect(t *testing.T) {
t.Run("should create handler with default pagination", func(t *testing.T) {
mockClient := &MockClient{}
handler := NewTeamMemberREST(mockClient, tracing.NewNoopTracerService(), featuremgmt.WithFeatures(featuremgmt.FlagKubernetesTeamBindings))
handler := NewUserTeamREST(mockClient, tracing.NewNoopTracerService(), featuremgmt.WithFeatures(featuremgmt.FlagKubernetesTeamBindings))
ctx := identity.WithRequester(context.Background(), &identity.StaticRequester{
Namespace: "test-namespace",
@ -68,7 +67,7 @@ func TestUserTeamREST_Connect(t *testing.T) {
t.Run("should parse limit query parameter", func(t *testing.T) {
mockClient := &MockClient{}
handler := NewTeamMemberREST(mockClient, tracing.NewNoopTracerService(), featuremgmt.WithFeatures(featuremgmt.FlagKubernetesTeamBindings))
handler := NewUserTeamREST(mockClient, tracing.NewNoopTracerService(), featuremgmt.WithFeatures(featuremgmt.FlagKubernetesTeamBindings))
ctx := identity.WithRequester(context.Background(), &identity.StaticRequester{
Namespace: "test-namespace",
@ -89,7 +88,7 @@ func TestUserTeamREST_Connect(t *testing.T) {
t.Run("should parse offset query parameter and calculate page", func(t *testing.T) {
mockClient := &MockClient{}
handler := NewTeamMemberREST(mockClient, tracing.NewNoopTracerService(), featuremgmt.WithFeatures(featuremgmt.FlagKubernetesTeamBindings))
handler := NewUserTeamREST(mockClient, tracing.NewNoopTracerService(), featuremgmt.WithFeatures(featuremgmt.FlagKubernetesTeamBindings))
ctx := identity.WithRequester(context.Background(), &identity.StaticRequester{
Namespace: "test-namespace",
@ -112,7 +111,7 @@ func TestUserTeamREST_Connect(t *testing.T) {
t.Run("should parse page query parameter and calculate offset", func(t *testing.T) {
mockClient := &MockClient{}
handler := NewTeamMemberREST(mockClient, tracing.NewNoopTracerService(), featuremgmt.WithFeatures(featuremgmt.FlagKubernetesTeamBindings))
handler := NewUserTeamREST(mockClient, tracing.NewNoopTracerService(), featuremgmt.WithFeatures(featuremgmt.FlagKubernetesTeamBindings))
ctx := identity.WithRequester(context.Background(), &identity.StaticRequester{
Namespace: "test-namespace",
@ -135,7 +134,7 @@ func TestUserTeamREST_Connect(t *testing.T) {
t.Run("should parse explain query parameter", func(t *testing.T) {
mockClient := &MockClient{}
handler := NewTeamMemberREST(mockClient, tracing.NewNoopTracerService(), featuremgmt.WithFeatures(featuremgmt.FlagKubernetesTeamBindings))
handler := NewUserTeamREST(mockClient, tracing.NewNoopTracerService(), featuremgmt.WithFeatures(featuremgmt.FlagKubernetesTeamBindings))
ctx := identity.WithRequester(context.Background(), &identity.StaticRequester{
Namespace: "test-namespace",
@ -156,7 +155,7 @@ func TestUserTeamREST_Connect(t *testing.T) {
t.Run("should not enable explain when explain=false", func(t *testing.T) {
mockClient := &MockClient{}
handler := NewTeamMemberREST(mockClient, tracing.NewNoopTracerService(), featuremgmt.WithFeatures(featuremgmt.FlagKubernetesTeamBindings))
handler := NewUserTeamREST(mockClient, tracing.NewNoopTracerService(), featuremgmt.WithFeatures(featuremgmt.FlagKubernetesTeamBindings))
ctx := identity.WithRequester(context.Background(), &identity.StaticRequester{
Namespace: "test-namespace",
@ -177,7 +176,7 @@ func TestUserTeamREST_Connect(t *testing.T) {
t.Run("should return error when identity is missing", func(t *testing.T) {
mockClient := &MockClient{}
handler := NewTeamMemberREST(mockClient, tracing.NewNoopTracerService(), featuremgmt.WithFeatures(featuremgmt.FlagKubernetesTeamBindings))
handler := NewUserTeamREST(mockClient, tracing.NewNoopTracerService(), featuremgmt.WithFeatures(featuremgmt.FlagKubernetesTeamBindings))
ctx := context.Background()
responder := &mockResponder{}
@ -200,7 +199,7 @@ func TestUserTeamREST_Connect(t *testing.T) {
mockClient := &MockClient{
MockError: errors.New("search failed"),
}
handler := NewTeamMemberREST(mockClient, tracing.NewNoopTracerService(), featuremgmt.WithFeatures(featuremgmt.FlagKubernetesTeamBindings))
handler := NewUserTeamREST(mockClient, tracing.NewNoopTracerService(), featuremgmt.WithFeatures(featuremgmt.FlagKubernetesTeamBindings))
ctx := identity.WithRequester(context.Background(), &identity.StaticRequester{
Namespace: "test-namespace",
@ -227,6 +226,7 @@ func TestUserTeamREST_Connect(t *testing.T) {
{
Results: &resourcepb.ResourceTable{
Columns: []*resourcepb.ResourceTableColumnDefinition{
{Name: "subject_name"},
{Name: "team_ref"},
{Name: "permission"},
{Name: "external"},
@ -234,6 +234,7 @@ func TestUserTeamREST_Connect(t *testing.T) {
Rows: []*resourcepb.ResourceTableRow{
{
Cells: [][]byte{
[]byte("user1"),
[]byte("team1"),
[]byte("admin"),
[]byte("true"),
@ -241,6 +242,7 @@ func TestUserTeamREST_Connect(t *testing.T) {
},
{
Cells: [][]byte{
[]byte("user2"),
[]byte("team2"),
[]byte("member"),
[]byte("false"),
@ -251,7 +253,7 @@ func TestUserTeamREST_Connect(t *testing.T) {
},
},
}
handler := NewTeamMemberREST(mockClient, tracing.NewNoopTracerService(), featuremgmt.WithFeatures(featuremgmt.FlagKubernetesTeamBindings))
handler := NewUserTeamREST(mockClient, tracing.NewNoopTracerService(), featuremgmt.WithFeatures(featuremgmt.FlagKubernetesTeamBindings))
ctx := identity.WithRequester(context.Background(), &identity.StaticRequester{
Namespace: "test-namespace",
@ -274,17 +276,19 @@ func TestUserTeamREST_Connect(t *testing.T) {
err = json.Unmarshal(w.Body.Bytes(), &result)
require.NoError(t, err)
require.Len(t, result.Items, 2)
require.Equal(t, "team1", result.Items[0].TeamRef.Name)
require.Equal(t, iamv0alpha1.TeamPermissionAdmin, result.Items[0].Permission)
require.Equal(t, "user1", result.Items[0].User)
require.Equal(t, "team1", result.Items[0].Team)
require.Equal(t, "admin", result.Items[0].Permission)
require.True(t, result.Items[0].External)
require.Equal(t, "team2", result.Items[1].TeamRef.Name)
require.Equal(t, iamv0alpha1.TeamPermissionMember, result.Items[1].Permission)
require.Equal(t, "user2", result.Items[1].User)
require.Equal(t, "team2", result.Items[1].Team)
require.Equal(t, "member", result.Items[1].Permission)
require.False(t, result.Items[1].External)
})
t.Run("should include correct fields in search request", func(t *testing.T) {
mockClient := &MockClient{}
handler := NewTeamMemberREST(mockClient, tracing.NewNoopTracerService(), featuremgmt.WithFeatures(featuremgmt.FlagKubernetesTeamBindings))
handler := NewUserTeamREST(mockClient, tracing.NewNoopTracerService(), featuremgmt.WithFeatures(featuremgmt.FlagKubernetesTeamBindings))
ctx := identity.WithRequester(context.Background(), &identity.StaticRequester{
Namespace: "test-namespace",
@ -301,6 +305,7 @@ func TestUserTeamREST_Connect(t *testing.T) {
httpHandler.ServeHTTP(w, req)
expectedFields := []string{
resource.SEARCH_FIELD_PREFIX + "subject_name",
resource.SEARCH_FIELD_PREFIX + "team_ref",
resource.SEARCH_FIELD_PREFIX + "permission",
resource.SEARCH_FIELD_PREFIX + "external",
@ -313,10 +318,8 @@ func TestUserTeamREST_Connect(t *testing.T) {
}
func TestUserTeamREST_parseResults(t *testing.T) {
handler := NewTeamMemberREST(nil, noop.NewTracerProvider().Tracer("test"), featuremgmt.WithFeatures(featuremgmt.FlagKubernetesTeamBindings))
t.Run("should return empty body when result is nil", func(t *testing.T) {
result, err := handler.parseResults(nil, 0)
result, err := parseResults(nil, 0)
require.NoError(t, err)
require.Empty(t, result.Items)
})
@ -331,7 +334,7 @@ func TestUserTeamREST_parseResults(t *testing.T) {
},
},
}
result, err := handler.parseResults(searchResult, 0)
result, err := parseResults(searchResult, 0)
require.Error(t, err)
require.Empty(t, result.Items)
require.Contains(t, err.Error(), "500 error searching")
@ -342,7 +345,7 @@ func TestUserTeamREST_parseResults(t *testing.T) {
searchResult := &resourcepb.ResourceSearchResponse{
Results: nil,
}
result, err := handler.parseResults(searchResult, 0)
result, err := parseResults(searchResult, 0)
require.NoError(t, err)
require.Empty(t, result.Items)
})
@ -351,13 +354,14 @@ func TestUserTeamREST_parseResults(t *testing.T) {
searchResult := &resourcepb.ResourceSearchResponse{
Results: &resourcepb.ResourceTable{
Columns: []*resourcepb.ResourceTableColumnDefinition{
{Name: "subject_name"},
{Name: "permission"},
{Name: "external"},
},
Rows: []*resourcepb.ResourceTableRow{},
},
}
result, err := handler.parseResults(searchResult, 0)
result, err := parseResults(searchResult, 0)
require.Error(t, err)
require.Contains(t, err.Error(), "required column 'team_ref' not found")
require.Empty(t, result.Items)
@ -367,13 +371,14 @@ func TestUserTeamREST_parseResults(t *testing.T) {
searchResult := &resourcepb.ResourceSearchResponse{
Results: &resourcepb.ResourceTable{
Columns: []*resourcepb.ResourceTableColumnDefinition{
{Name: "subject_name"},
{Name: "team_ref"},
{Name: "external"},
},
Rows: []*resourcepb.ResourceTableRow{},
},
}
result, err := handler.parseResults(searchResult, 0)
result, err := parseResults(searchResult, 0)
require.Error(t, err)
require.Contains(t, err.Error(), "required column 'permission' not found")
require.Empty(t, result.Items)
@ -383,19 +388,20 @@ func TestUserTeamREST_parseResults(t *testing.T) {
searchResult := &resourcepb.ResourceSearchResponse{
Results: &resourcepb.ResourceTable{
Columns: []*resourcepb.ResourceTableColumnDefinition{
{Name: "subject_name"},
{Name: "team_ref"},
{Name: "permission"},
},
Rows: []*resourcepb.ResourceTableRow{},
},
}
result, err := handler.parseResults(searchResult, 0)
result, err := parseResults(searchResult, 0)
require.Error(t, err)
require.Contains(t, err.Error(), "required column 'external' not found")
require.Empty(t, result.Items)
})
t.Run("should parse valid results correctly", func(t *testing.T) {
t.Run("should return error when subject_name column is missing", func(t *testing.T) {
searchResult := &resourcepb.ResourceSearchResponse{
Results: &resourcepb.ResourceTable{
Columns: []*resourcepb.ResourceTableColumnDefinition{
@ -403,9 +409,27 @@ func TestUserTeamREST_parseResults(t *testing.T) {
{Name: "permission"},
{Name: "external"},
},
},
}
result, err := parseResults(searchResult, 0)
require.Error(t, err)
require.Contains(t, err.Error(), "required column 'subject_name' not found")
require.Empty(t, result.Items)
})
t.Run("should parse valid results correctly", func(t *testing.T) {
searchResult := &resourcepb.ResourceSearchResponse{
Results: &resourcepb.ResourceTable{
Columns: []*resourcepb.ResourceTableColumnDefinition{
{Name: "subject_name"},
{Name: "team_ref"},
{Name: "permission"},
{Name: "external"},
},
Rows: []*resourcepb.ResourceTableRow{
{
Cells: [][]byte{
[]byte("user1"),
[]byte("team1"),
[]byte("admin"),
[]byte("true"),
@ -413,6 +437,7 @@ func TestUserTeamREST_parseResults(t *testing.T) {
},
{
Cells: [][]byte{
[]byte("user2"),
[]byte("team2"),
[]byte("member"),
[]byte("false"),
@ -421,16 +446,18 @@ func TestUserTeamREST_parseResults(t *testing.T) {
},
},
}
result, err := handler.parseResults(searchResult, 0)
result, err := parseResults(searchResult, 0)
require.NoError(t, err)
require.Len(t, result.Items, 2)
require.Equal(t, "team1", result.Items[0].TeamRef.Name)
require.Equal(t, iamv0alpha1.TeamPermissionAdmin, result.Items[0].Permission)
require.Equal(t, "user1", result.Items[0].User)
require.Equal(t, "team1", result.Items[0].Team)
require.Equal(t, "admin", result.Items[0].Permission)
require.True(t, result.Items[0].External)
require.Equal(t, "team2", result.Items[1].TeamRef.Name)
require.Equal(t, iamv0alpha1.TeamPermissionMember, result.Items[1].Permission)
require.Equal(t, "user2", result.Items[1].User)
require.Equal(t, "team2", result.Items[1].Team)
require.Equal(t, "member", result.Items[1].Permission)
require.False(t, result.Items[1].External)
})
@ -438,6 +465,7 @@ func TestUserTeamREST_parseResults(t *testing.T) {
searchResult := &resourcepb.ResourceSearchResponse{
Results: &resourcepb.ResourceTable{
Columns: []*resourcepb.ResourceTableColumnDefinition{
{Name: "subject_name"},
{Name: "team_ref"},
{Name: "permission"},
{Name: "external"},
@ -445,6 +473,7 @@ func TestUserTeamREST_parseResults(t *testing.T) {
Rows: []*resourcepb.ResourceTableRow{
{
Cells: [][]byte{
[]byte("user1"),
[]byte("team1"),
[]byte("admin"),
// Missing external cell
@ -453,7 +482,7 @@ func TestUserTeamREST_parseResults(t *testing.T) {
},
},
}
result, err := handler.parseResults(searchResult, 0)
result, err := parseResults(searchResult, 0)
require.Error(t, err)
require.Contains(t, err.Error(), "mismatch number of columns and cells")
require.Empty(t, result.Items)
@ -464,6 +493,7 @@ func TestUserTeamREST_parseResults(t *testing.T) {
Results: &resourcepb.ResourceTable{
Columns: []*resourcepb.ResourceTableColumnDefinition{
nil,
{Name: "subject_name"},
{Name: "team_ref"},
{Name: "permission"},
{Name: "external"},
@ -472,6 +502,7 @@ func TestUserTeamREST_parseResults(t *testing.T) {
{
Cells: [][]byte{
[]byte(""),
[]byte("user1"),
[]byte("team1"),
[]byte("admin"),
[]byte("true"),
@ -480,9 +511,12 @@ func TestUserTeamREST_parseResults(t *testing.T) {
},
},
}
result, err := handler.parseResults(searchResult, 0)
result, err := parseResults(searchResult, 0)
require.NoError(t, err)
require.Len(t, result.Items, 1)
require.Equal(t, "team1", result.Items[0].TeamRef.Name)
require.Equal(t, "user1", result.Items[0].User)
require.Equal(t, "team1", result.Items[0].Team)
require.Equal(t, "admin", result.Items[0].Permission)
require.True(t, result.Items[0].External)
})
}

View file

@ -20,9 +20,8 @@ import (
type userTeamsResponse struct {
Items []struct {
TeamRef struct {
Name string `json:"name"`
} `json:"teamRef"`
Team string `json:"team"`
User string `json:"user"`
Permission string `json:"permission"`
External bool `json:"external"`
} `json:"items"`
@ -60,12 +59,12 @@ func TestIntegrationUserTeams(t *testing.T) {
t.Cleanup(func() { helper.Shutdown() })
doUserTeamsTests(t, helper, mode)
doUserTeamsTests(t, helper)
})
}
}
func doUserTeamsTests(t *testing.T, helper *apis.K8sTestHelper, mode rest.DualWriterMode) {
func doUserTeamsTests(t *testing.T, helper *apis.K8sTestHelper) {
ctx := context.Background()
orgNS := helper.Namespacer(helper.Org1.Admin.Identity.GetOrgID())
@ -123,7 +122,7 @@ func doUserTeamsTests(t *testing.T, helper *apis.K8sTestHelper, mode rest.DualWr
require.NoError(t, err)
}
t.Run(fmt.Sprintf("returns the bound team for the user with dual writer mode %d", mode), func(t *testing.T) {
t.Run("returns the bound team for the user", func(t *testing.T) {
path := fmt.Sprintf("/apis/iam.grafana.app/v0alpha1/namespaces/default/users/%s/teams", u1.GetName())
var res userTeamsResponse
@ -137,11 +136,13 @@ func doUserTeamsTests(t *testing.T, helper *apis.K8sTestHelper, mode rest.DualWr
require.True(t, containsTeam(res, teams[0].GetName()), "expected response to contain team %q, got %#v", teams[0].GetName(), res.Items)
item := findTeam(res, teams[0].GetName())
require.Equal(t, u1.GetName(), item.User)
require.Equal(t, teams[0].GetName(), item.Team)
require.Equal(t, "admin", item.Permission)
require.False(t, item.External)
})
t.Run(fmt.Sprintf("does not return the bound team for a different user with dual writer mode %d", mode), func(t *testing.T) {
t.Run("does not return the bound team for a different user", func(t *testing.T) {
path := fmt.Sprintf("/apis/iam.grafana.app/v0alpha1/namespaces/default/users/%s/teams", u2.GetName())
var res userTeamsResponse
@ -155,7 +156,7 @@ func doUserTeamsTests(t *testing.T, helper *apis.K8sTestHelper, mode rest.DualWr
require.False(t, containsTeam(res, teams[0].GetName()), "did not expect response to contain team %q, got %#v", teams[0].GetName(), res.Items)
})
t.Run(fmt.Sprintf("paging with page and limit with dual writer mode %d", mode), func(t *testing.T) {
t.Run("paging with page and limit", func(t *testing.T) {
// Page 1, Limit 2
res1 := getUserTeamsWithPaging(t, helper, u1.GetName(), 1, 2)
require.Len(t, res1.Items, 2)
@ -170,24 +171,24 @@ func doUserTeamsTests(t *testing.T, helper *apis.K8sTestHelper, mode rest.DualWr
seen := make(map[string]bool)
for _, item := range res1.Items {
teamName := item.TeamRef.Name
teamName := item.Team
require.False(t, seen[teamName], "Team %s seen in page 1 twice", teamName)
seen[teamName] = true
}
for _, item := range res2.Items {
teamName := item.TeamRef.Name
teamName := item.Team
require.False(t, seen[teamName], "Team %s seen in page 1 and 2", teamName)
seen[teamName] = true
}
for _, item := range res3.Items {
teamName := item.TeamRef.Name
teamName := item.Team
require.False(t, seen[teamName], "Team %s seen in previous pages", teamName)
seen[teamName] = true
}
require.Len(t, seen, 5, "Should have seen all 5 teams across pages")
})
t.Run(fmt.Sprintf("paging with offset and limit with dual writer mode %d", mode), func(t *testing.T) {
t.Run("paging with offset and limit", func(t *testing.T) {
// Offset 0, Limit 2
res1 := getUserTeamsWithOffset(t, helper, u1.GetName(), 0, 2)
require.Len(t, res1.Items, 2)
@ -202,17 +203,17 @@ func doUserTeamsTests(t *testing.T, helper *apis.K8sTestHelper, mode rest.DualWr
seen := make(map[string]bool)
for _, item := range res1.Items {
teamName := item.TeamRef.Name
teamName := item.Team
require.False(t, seen[teamName], "Team %s seen in offset 0 twice", teamName)
seen[teamName] = true
}
for _, item := range res2.Items {
teamName := item.TeamRef.Name
teamName := item.Team
require.False(t, seen[teamName], "Team %s seen in offset 0 and 2", teamName)
seen[teamName] = true
}
for _, item := range res3.Items {
teamName := item.TeamRef.Name
teamName := item.Team
require.False(t, seen[teamName], "Team %s seen in previous offsets", teamName)
seen[teamName] = true
}
@ -231,7 +232,7 @@ func createTeamObject(helper *apis.K8sTestHelper, teamName string, title string,
func containsTeam(res userTeamsResponse, teamName string) bool {
for _, it := range res.Items {
if it.TeamRef.Name == teamName {
if it.Team == teamName {
return true
}
}
@ -239,14 +240,13 @@ func containsTeam(res userTeamsResponse, teamName string) bool {
}
func findTeam(res userTeamsResponse, teamName string) (out struct {
TeamRef struct {
Name string `json:"name"`
} `json:"teamRef"`
Team string `json:"team"`
User string `json:"user"`
Permission string `json:"permission"`
External bool `json:"external"`
}) {
for _, it := range res.Items {
if it.TeamRef.Name == teamName {
if it.Team == teamName {
return it
}
}

View file

@ -7741,19 +7741,6 @@
}
}
},
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamRef": {
"type": "object",
"required": [
"name"
],
"properties": {
"name": {
"description": "Name is the unique identifier for a team.",
"type": "string",
"default": ""
}
}
},
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.TeamSpec": {
"type": "object",
"required": [
@ -8001,7 +7988,8 @@
"github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1.VersionsV0alpha1Kinds6RoutesTeamsGETResponseUserTeam": {
"type": "object",
"required": [
"teamRef",
"user",
"team",
"permission",
"external"
],
@ -8014,8 +8002,13 @@
"type": "string",
"default": ""
},
"teamRef": {
"default": {}
"team": {
"type": "string",
"default": ""
},
"user": {
"type": "string",
"default": ""
}
}
},