diff --git a/pkg/registry/apis/iam/legacy/team_binding.go b/pkg/registry/apis/iam/legacy/team_binding.go index 2928b213ee2..d2d208777c1 100644 --- a/pkg/registry/apis/iam/legacy/team_binding.go +++ b/pkg/registry/apis/iam/legacy/team_binding.go @@ -17,6 +17,8 @@ import ( type ListTeamBindingsQuery struct { UID string OrgID int64 + TeamUID string + UserUID string Pagination common.Pagination } diff --git a/pkg/registry/apis/iam/legacy/team_bindings_query.sql b/pkg/registry/apis/iam/legacy/team_bindings_query.sql index 3e5dd92beea..7a5480c0fb6 100644 --- a/pkg/registry/apis/iam/legacy/team_bindings_query.sql +++ b/pkg/registry/apis/iam/legacy/team_bindings_query.sql @@ -7,6 +7,12 @@ WHERE {{ if .Query.UID }} AND tm.uid = {{ .Arg .Query.UID }} {{ end }} + {{ if .Query.TeamUID }} + AND t.uid = {{ .Arg .Query.TeamUID }} + {{ end }} + {{ if .Query.UserUID }} + AND u.uid = {{ .Arg .Query.UserUID }} + {{ end }} {{- if .Query.Pagination.Continue }} AND tm.id >= {{ .Arg .Query.Pagination.Continue }} {{- end }} diff --git a/pkg/registry/apis/iam/teambinding/store.go b/pkg/registry/apis/iam/teambinding/store.go index 6f6fd4ce162..77582eaaf44 100644 --- a/pkg/registry/apis/iam/teambinding/store.go +++ b/pkg/registry/apis/iam/teambinding/store.go @@ -276,9 +276,20 @@ func (l *LegacyBindingStore) List(ctx context.Context, options *internalversion. return nil, err } - res, err := l.store.ListTeamBindings(ctx, ns, legacy.ListTeamBindingsQuery{ + query := legacy.ListTeamBindingsQuery{ Pagination: common.PaginationFromListOptions(options), - }) + } + + if options.FieldSelector != nil { + if name, ok := options.FieldSelector.RequiresExactMatch("spec.teamRef.name"); ok { + query.TeamUID = name + } + if name, ok := options.FieldSelector.RequiresExactMatch("spec.subject.name"); ok { + query.UserUID = name + } + } + + res, err := l.store.ListTeamBindings(ctx, ns, query) if err != nil { return nil, err } diff --git a/pkg/tests/apis/iam/team_bindings_integration_test.go b/pkg/tests/apis/iam/team_bindings_integration_test.go index 1b355296486..b5b528f309f 100644 --- a/pkg/tests/apis/iam/team_bindings_integration_test.go +++ b/pkg/tests/apis/iam/team_bindings_integration_test.go @@ -10,6 +10,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + iamv0alpha1 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1" "github.com/grafana/grafana/pkg/apiserver/rest" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/setting" @@ -65,6 +66,7 @@ func TestIntegrationTeamBindings(t *testing.T) { require.NotNil(t, user) doTeamBindingCRUDTestsUsingTheNewAPIs(t, helper, team, user) + doTeamBindingFieldSelectionTests(t, helper) if mode < 3 { doTeamBindingCRUDTestsUsingTheLegacyAPIs(t, helper, mode) @@ -101,16 +103,17 @@ func doTeamBindingCRUDTestsUsingTheNewAPIs(t *testing.T, helper *apis.K8sTestHel require.NotEmpty(t, createdUID) // Get the team binding - fetched, err := teamBindingClient.Resource.Get(ctx, createdUID, metav1.GetOptions{}) + response, err := teamBindingClient.Resource.Get(ctx, createdUID, metav1.GetOptions{}) require.NoError(t, err) - require.NotNil(t, fetched) + require.NotNil(t, response) - fetchedSpec := fetched.Object["spec"].(map[string]interface{}) - require.Equal(t, user.GetName(), fetchedSpec["subject"].(map[string]interface{})["name"]) - require.Equal(t, team.GetName(), fetchedSpec["teamRef"].(map[string]interface{})["name"]) - require.Equal(t, "admin", fetchedSpec["permission"]) - require.Equal(t, false, fetchedSpec["external"]) - require.Equal(t, createdUID, fetched.GetName()) + var actual iamv0alpha1.TeamBinding + require.NoError(t, helper.Scheme.Convert(response, &actual, nil)) + require.Equal(t, user.GetName(), actual.Spec.Subject.Name) + require.Equal(t, team.GetName(), actual.Spec.TeamRef.Name) + require.Equal(t, iamv0alpha1.TeamBindingTeamPermissionAdmin, actual.Spec.Permission) + require.False(t, actual.Spec.External) + require.Equal(t, createdUID, actual.Name) // Update the team binding toUpdate := toCreate.DeepCopy() @@ -127,16 +130,16 @@ func doTeamBindingCRUDTestsUsingTheNewAPIs(t *testing.T, helper *apis.K8sTestHel require.Equal(t, false, updatedSpec["external"]) // Get the team binding - fetched, err = teamBindingClient.Resource.Get(ctx, createdUID, metav1.GetOptions{}) + response, err = teamBindingClient.Resource.Get(ctx, createdUID, metav1.GetOptions{}) require.NoError(t, err) - require.NotNil(t, fetched) + require.NotNil(t, response) - fetchedSpec = fetched.Object["spec"].(map[string]interface{}) - require.Equal(t, user.GetName(), fetchedSpec["subject"].(map[string]interface{})["name"]) - require.Equal(t, team.GetName(), fetchedSpec["teamRef"].(map[string]interface{})["name"]) - require.Equal(t, "member", fetchedSpec["permission"]) - require.Equal(t, false, fetchedSpec["external"]) - require.Equal(t, createdUID, fetched.GetName()) + require.NoError(t, helper.Scheme.Convert(response, &actual, nil)) + require.Equal(t, user.GetName(), actual.Spec.Subject.Name) + require.Equal(t, team.GetName(), actual.Spec.TeamRef.Name) + require.Equal(t, iamv0alpha1.TeamBindingTeamPermissionMember, actual.Spec.Permission) + require.False(t, actual.Spec.External) + require.Equal(t, createdUID, actual.Name) // Delete the team binding err = teamBindingClient.Resource.Delete(ctx, createdUID, metav1.DeleteOptions{}) @@ -488,14 +491,136 @@ func doTeamBindingCRUDTestsUsingTheLegacyAPIs(t *testing.T, helper *apis.K8sTest GVR: gvrTeamBindings, }) - teamBinding, err := teamBindingClient.Resource.Get(ctx, teamBindingName, metav1.GetOptions{}) + response, err := teamBindingClient.Resource.Get(ctx, teamBindingName, metav1.GetOptions{}) require.NoError(t, err) - require.NotNil(t, teamBinding) + require.NotNil(t, response) - teamBindingSpec := teamBinding.Object["spec"].(map[string]interface{}) - require.Equal(t, "member", teamBindingSpec["permission"]) - require.Equal(t, userRsp.Result.UID, teamBindingSpec["subject"].(map[string]interface{})["name"]) - require.Equal(t, teamRsp.Result.UID, teamBindingSpec["teamRef"].(map[string]interface{})["name"]) - require.Equal(t, teamBindingName, teamBinding.GetName()) + var actual iamv0alpha1.TeamBinding + require.NoError(t, helper.Scheme.Convert(response, &actual, nil)) + require.Equal(t, iamv0alpha1.TeamBindingTeamPermissionMember, actual.Spec.Permission) + require.Equal(t, userRsp.Result.UID, actual.Spec.Subject.Name) + require.Equal(t, teamRsp.Result.UID, actual.Spec.TeamRef.Name) + require.Equal(t, teamBindingName, actual.Name) + }) +} + +func doTeamBindingFieldSelectionTests(t *testing.T, helper *apis.K8sTestHelper) { + t.Run("should list team bindings using field selectors", func(t *testing.T) { + ctx := context.Background() + + var teamNames []string + var userNames []string + var bindingNames []string + + teamBindingClient := helper.GetResourceClient(apis.ResourceClientArgs{ + User: helper.Org1.Admin, + Namespace: helper.Namespacer(helper.Org1.Admin.Identity.GetOrgID()), + GVR: gvrTeamBindings, + }) + teamClient := helper.GetResourceClient(apis.ResourceClientArgs{ + User: helper.Org1.Admin, + Namespace: helper.Namespacer(helper.Org1.Admin.Identity.GetOrgID()), + GVR: gvrTeams, + }) + userClient := helper.GetResourceClient(apis.ResourceClientArgs{ + User: helper.Org1.Admin, + Namespace: helper.Namespacer(helper.Org1.Admin.Identity.GetOrgID()), + GVR: gvrUsers, + }) + + // Helper to create teams + createTeam := func(name string, email string) *unstructured.Unstructured { + obj := helper.LoadYAMLOrJSONFile("testdata/team-test-create-v0.yaml") + obj.SetName(name) + if email != "" { + obj.Object["spec"].(map[string]interface{})["email"] = email + } + + created, err := teamClient.Resource.Create(ctx, obj, metav1.CreateOptions{}) + require.NoError(t, err) + teamNames = append(teamNames, created.GetName()) + return created + } + + // Helper to create users + createUser := func(name string, email string, login string) *unstructured.Unstructured { + obj := helper.LoadYAMLOrJSONFile("testdata/user-test-create-v0.yaml") + obj.SetName(name) + + spec := obj.Object["spec"].(map[string]interface{}) + spec["email"] = email + spec["login"] = login + + created, err := userClient.Resource.Create(ctx, obj, metav1.CreateOptions{}) + require.NoError(t, err) + userNames = append(userNames, created.GetName()) + return created + } + + teamA := createTeam("team-a", "teama@example.com") + teamB := createTeam("team-b", "teamb@example.com") + user1 := createUser("user-1", "user1@example.com", "user1") + user2 := createUser("user-2", "user2@example.com", "user2") + + createBinding := func(user *unstructured.Unstructured, team *unstructured.Unstructured) { + toCreate := helper.LoadYAMLOrJSONFile("testdata/teambinding-test-create-v0.yaml") + toCreate.SetName("") + toCreate.SetGenerateName("binding-") + toCreate.Object["spec"].(map[string]interface{})["subject"].(map[string]interface{})["name"] = user.GetName() + toCreate.Object["spec"].(map[string]interface{})["teamRef"].(map[string]interface{})["name"] = team.GetName() + + created, err := teamBindingClient.Resource.Create(ctx, toCreate, metav1.CreateOptions{}) + require.NoError(t, err) + bindingNames = append(bindingNames, created.GetName()) + } + + // Create 4 bindings + createBinding(user1, teamA) + createBinding(user2, teamA) + createBinding(user1, teamB) + createBinding(user2, teamB) + + t.Cleanup(func() { + cleanupCtx := context.Background() + + for _, name := range bindingNames { + _, _ = teamBindingClient.Resource.Delete(cleanupCtx, name, metav1.DeleteOptions{}) + } + for _, name := range teamNames { + _, _ = teamClient.Resource.Delete(cleanupCtx, name, metav1.DeleteOptions{}) + } + for _, name := range userNames { + _, _ = userClient.Resource.Delete(cleanupCtx, name, metav1.DeleteOptions{}) + } + }) + + // Verify we have at least 4 bindings overall + all, err := teamBindingClient.Resource.List(ctx, metav1.ListOptions{}) + require.NoError(t, err) + require.GreaterOrEqual(t, len(all.Items), 4) + + // Query 1: select by teamRef.name, should return 2 of the 4 + listByTeam, err := teamBindingClient.Resource.List(ctx, metav1.ListOptions{ + FieldSelector: fmt.Sprintf("spec.teamRef.name=%s", teamA.GetName()), + }) + require.NoError(t, err) + require.Len(t, listByTeam.Items, 2) + for _, item := range listByTeam.Items { + var actual iamv0alpha1.TeamBinding + require.NoError(t, helper.Scheme.Convert(&item, &actual, nil)) + require.Equal(t, teamA.GetName(), actual.Spec.TeamRef.Name) + } + + // Query 2: select by subject.name, should return 2 of the 4 + listByUser, err := teamBindingClient.Resource.List(ctx, metav1.ListOptions{ + FieldSelector: fmt.Sprintf("spec.subject.name=%s", user1.GetName()), + }) + require.NoError(t, err) + require.Len(t, listByUser.Items, 2) + for _, item := range listByUser.Items { + var actual iamv0alpha1.TeamBinding + require.NoError(t, helper.Scheme.Convert(&item, &actual, nil)) + require.Equal(t, user1.GetName(), actual.Spec.Subject.Name) + } }) }