Compare commits

...

2 Commits

Author SHA1 Message Date
Mihai Doarna e189d8547f fix validation test
CodeQL checks / Detect whether code changed (push) Waiting to run Details
CodeQL checks / Analyze (actions) (push) Blocked by required conditions Details
CodeQL checks / Analyze (go) (push) Blocked by required conditions Details
CodeQL checks / Analyze (javascript) (push) Blocked by required conditions Details
2025-10-07 18:09:05 +03:00
Mihai Doarna 9e07387de2 add integration tests and fix remaining issues 2025-10-07 17:45:18 +03:00
26 changed files with 473 additions and 46 deletions

View File

@ -24,6 +24,7 @@ func newIAMAuthorizer(accessClient authlib.AccessClient, legacyAccessClient auth
// Identity specific resources
legacyAuthorizer := gfauthorizer.NewResourceAuthorizer(legacyAccessClient)
resourceAuthorizer[iamv0.TeamResourceInfo.GetName()] = legacyAuthorizer
resourceAuthorizer[iamv0.TeamBindingResourceInfo.GetName()] = legacyAuthorizer
resourceAuthorizer["display"] = legacyAuthorizer
// Access specific resources

View File

@ -1,5 +1,5 @@
INSERT INTO {{ .Ident .TeamMemberTable }}
(team_id, user_id, created, updated, external, permission)
(team_id, user_id, org_id, created, updated, external, permission)
VALUES
({{ .Arg .Command.TeamID }}, {{ .Arg .Command.UserID }}, {{ .Arg .Command.Created }},
({{ .Arg .Command.TeamID }}, {{ .Arg .Command.UserID }}, {{ .Arg .Command.OrgID }}, {{ .Arg .Command.Created }},
{{ .Arg .Command.Updated }}, {{ .Arg .Command.External }}, {{ .Arg .Command.Permission }})

View File

@ -226,7 +226,8 @@ func TestIdentityQueries(t *testing.T) {
Name: "team_1_bindings",
Data: listTeamBindings(&ListTeamBindingsQuery{
OrgID: 1,
UID: "team-1",
TeamID: 1,
UserID: 1,
Pagination: common.Pagination{Limit: 1},
}),
},

View File

@ -414,8 +414,8 @@ func (s *legacySQLStore) DeleteTeam(ctx context.Context, ns claims.NamespaceInfo
}
type ListTeamBindingsQuery struct {
// UID is team uid to list bindings for. If not set store should list bindings for all teams
UID string
TeamID int64
UserID int64
OrgID int64
Pagination common.Pagination
}
@ -432,6 +432,7 @@ type TeamMember struct {
TeamUID string
UserID int64
UserUID string
OrgID int64
Name string
Email string
Username string
@ -486,7 +487,7 @@ func (s *legacySQLStore) ListTeamBindings(ctx context.Context, ns claims.Namespa
req := newListTeamBindings(sql, &query)
q, err := sqltemplate.Execute(sqlQueryTeamBindingsTemplate, req)
if err != nil {
return nil, fmt.Errorf("execute template %q: %w", sqlQueryTeamsTemplate.Name(), err)
return nil, fmt.Errorf("execute template %q: %w", sqlQueryTeamBindingsTemplate.Name(), err)
}
rows, err := sql.DB.GetSqlxSession().Query(ctx, q, req.GetArgs()...)
@ -508,7 +509,7 @@ func (s *legacySQLStore) ListTeamBindings(ctx context.Context, ns claims.Namespa
for rows.Next() {
m := TeamMember{}
err = rows.Scan(&m.ID, &m.TeamUID, &m.TeamID, &m.UserUID, &m.Created, &m.Updated, &m.Permission)
err = rows.Scan(&m.ID, &m.TeamUID, &m.TeamID, &m.UserUID, &m.UserID, &m.Created, &m.Updated, &m.Permission)
if err != nil {
return res, err
}
@ -522,16 +523,15 @@ func (s *legacySQLStore) ListTeamBindings(ctx context.Context, ns claims.Namespa
}
}
if query.UID == "" {
res.RV, err = sql.GetResourceVersion(ctx, "team_member", "updated")
}
return res, err
}
type CreateTeamMemberCommand struct {
TeamID int64
TeamUID string
UserID int64
UserUID string
OrgID int64
Created DBTime
Updated DBTime
External bool
@ -566,6 +566,11 @@ func (s *legacySQLStore) CreateTeamMember(ctx context.Context, ns claims.Namespa
now := time.Now().UTC().Truncate(time.Second)
cmd.Created = NewDBTime(now)
cmd.Updated = NewDBTime(now)
cmd.OrgID = ns.OrgID
if cmd.OrgID == 0 {
return nil, fmt.Errorf("expected non zero org id")
}
sql, err := s.sql(ctx)
if err != nil {
@ -589,7 +594,10 @@ func (s *legacySQLStore) CreateTeamMember(ctx context.Context, ns claims.Namespa
createdTeamMember = TeamMember{
ID: teamMemberID,
TeamID: cmd.TeamID,
TeamUID: cmd.TeamUID,
UserID: cmd.UserID,
UserUID: cmd.UserUID,
OrgID: cmd.OrgID,
Created: cmd.Created.Time,
Updated: cmd.Updated.Time,
External: cmd.External,

View File

@ -1,11 +1,14 @@
SELECT tm.id as id, t.uid as team_uid, t.id as team_id, u.uid as user_uid, tm.created, tm.updated, tm.permission
SELECT tm.id as id, t.uid as team_uid, t.id as team_id, u.uid as user_uid, u.id as user_id, tm.created, tm.updated, tm.permission
FROM {{ .Ident .TeamMemberTable }} tm
INNER JOIN {{ .Ident .TeamTable }} t ON tm.team_id = t.id
INNER JOIN {{ .Ident .UserTable }} u ON tm.user_id = u.id
WHERE
tm.org_id = {{ .Arg .Query.OrgID}}
{{ if .Query.UID }}
AND t.uid = {{ .Arg .Query.UID }}
{{ if .Query.TeamID }}
AND tm.team_id = {{ .Arg .Query.TeamID }}
{{ end }}
{{ if .Query.UserID }}
AND tm.user_id = {{ .Arg .Query.UserID }}
{{ end }}
{{- if .Query.Pagination.Continue }}
AND tm.id >= {{ .Arg .Query.Pagination.Continue }}

View File

@ -1,5 +1,5 @@
INSERT INTO `grafana`.`team_member`
(team_id, user_id, created, updated, external, permission)
(team_id, user_id, org_id, created, updated, external, permission)
VALUES
(1, 1, '2023-01-01 12:00:00',
(1, 1, 0, '2023-01-01 12:00:00',
'2023-01-01 12:00:00', FALSE, 'Member')

View File

@ -1,5 +1,5 @@
INSERT INTO `grafana`.`team_member`
(team_id, user_id, created, updated, external, permission)
(team_id, user_id, org_id, created, updated, external, permission)
VALUES
(1, 1, '2023-01-01 12:00:00',
(1, 1, 0, '2023-01-01 12:00:00',
'2023-01-01 12:00:00', FALSE, 'Admin')

View File

@ -1,10 +1,11 @@
SELECT tm.id as id, t.uid as team_uid, t.id as team_id, u.uid as user_uid, tm.created, tm.updated, tm.permission
SELECT tm.id as id, t.uid as team_uid, t.id as team_id, u.uid as user_uid, u.id as user_id, tm.created, tm.updated, tm.permission
FROM `grafana`.`team_member` tm
INNER JOIN `grafana`.`team` t ON tm.team_id = t.id
INNER JOIN `grafana`.`user` u ON tm.user_id = u.id
WHERE
tm.org_id = 1
AND t.uid = 'team-1'
AND tm.team_id = 1
AND tm.user_id = 1
AND NOT tm.external
ORDER BY t.id ASC
LIMIT 1;

View File

@ -1,4 +1,4 @@
SELECT tm.id as id, t.uid as team_uid, t.id as team_id, u.uid as user_uid, tm.created, tm.updated, tm.permission
SELECT tm.id as id, t.uid as team_uid, t.id as team_id, u.uid as user_uid, u.id as user_id, tm.created, tm.updated, tm.permission
FROM `grafana`.`team_member` tm
INNER JOIN `grafana`.`team` t ON tm.team_id = t.id
INNER JOIN `grafana`.`user` u ON tm.user_id = u.id

View File

@ -1,4 +1,4 @@
SELECT tm.id as id, t.uid as team_uid, t.id as team_id, u.uid as user_uid, tm.created, tm.updated, tm.permission
SELECT tm.id as id, t.uid as team_uid, t.id as team_id, u.uid as user_uid, u.id as user_id, tm.created, tm.updated, tm.permission
FROM `grafana`.`team_member` tm
INNER JOIN `grafana`.`team` t ON tm.team_id = t.id
INNER JOIN `grafana`.`user` u ON tm.user_id = u.id

View File

@ -1,5 +1,5 @@
INSERT INTO "grafana"."team_member"
(team_id, user_id, created, updated, external, permission)
(team_id, user_id, org_id, created, updated, external, permission)
VALUES
(1, 1, '2023-01-01 12:00:00',
(1, 1, 0, '2023-01-01 12:00:00',
'2023-01-01 12:00:00', FALSE, 'Member')

View File

@ -1,5 +1,5 @@
INSERT INTO "grafana"."team_member"
(team_id, user_id, created, updated, external, permission)
(team_id, user_id, org_id, created, updated, external, permission)
VALUES
(1, 1, '2023-01-01 12:00:00',
(1, 1, 0, '2023-01-01 12:00:00',
'2023-01-01 12:00:00', FALSE, 'Admin')

View File

@ -1,10 +1,11 @@
SELECT tm.id as id, t.uid as team_uid, t.id as team_id, u.uid as user_uid, tm.created, tm.updated, tm.permission
SELECT tm.id as id, t.uid as team_uid, t.id as team_id, u.uid as user_uid, u.id as user_id, tm.created, tm.updated, tm.permission
FROM "grafana"."team_member" tm
INNER JOIN "grafana"."team" t ON tm.team_id = t.id
INNER JOIN "grafana"."user" u ON tm.user_id = u.id
WHERE
tm.org_id = 1
AND t.uid = 'team-1'
AND tm.team_id = 1
AND tm.user_id = 1
AND NOT tm.external
ORDER BY t.id ASC
LIMIT 1;

View File

@ -1,4 +1,4 @@
SELECT tm.id as id, t.uid as team_uid, t.id as team_id, u.uid as user_uid, tm.created, tm.updated, tm.permission
SELECT tm.id as id, t.uid as team_uid, t.id as team_id, u.uid as user_uid, u.id as user_id, tm.created, tm.updated, tm.permission
FROM "grafana"."team_member" tm
INNER JOIN "grafana"."team" t ON tm.team_id = t.id
INNER JOIN "grafana"."user" u ON tm.user_id = u.id

View File

@ -1,4 +1,4 @@
SELECT tm.id as id, t.uid as team_uid, t.id as team_id, u.uid as user_uid, tm.created, tm.updated, tm.permission
SELECT tm.id as id, t.uid as team_uid, t.id as team_id, u.uid as user_uid, u.id as user_id, tm.created, tm.updated, tm.permission
FROM "grafana"."team_member" tm
INNER JOIN "grafana"."team" t ON tm.team_id = t.id
INNER JOIN "grafana"."user" u ON tm.user_id = u.id

View File

@ -1,5 +1,5 @@
INSERT INTO "grafana"."team_member"
(team_id, user_id, created, updated, external, permission)
(team_id, user_id, org_id, created, updated, external, permission)
VALUES
(1, 1, '2023-01-01 12:00:00',
(1, 1, 0, '2023-01-01 12:00:00',
'2023-01-01 12:00:00', FALSE, 'Member')

View File

@ -1,5 +1,5 @@
INSERT INTO "grafana"."team_member"
(team_id, user_id, created, updated, external, permission)
(team_id, user_id, org_id, created, updated, external, permission)
VALUES
(1, 1, '2023-01-01 12:00:00',
(1, 1, 0, '2023-01-01 12:00:00',
'2023-01-01 12:00:00', FALSE, 'Admin')

View File

@ -1,10 +1,11 @@
SELECT tm.id as id, t.uid as team_uid, t.id as team_id, u.uid as user_uid, tm.created, tm.updated, tm.permission
SELECT tm.id as id, t.uid as team_uid, t.id as team_id, u.uid as user_uid, u.id as user_id, tm.created, tm.updated, tm.permission
FROM "grafana"."team_member" tm
INNER JOIN "grafana"."team" t ON tm.team_id = t.id
INNER JOIN "grafana"."user" u ON tm.user_id = u.id
WHERE
tm.org_id = 1
AND t.uid = 'team-1'
AND tm.team_id = 1
AND tm.user_id = 1
AND NOT tm.external
ORDER BY t.id ASC
LIMIT 1;

View File

@ -1,4 +1,4 @@
SELECT tm.id as id, t.uid as team_uid, t.id as team_id, u.uid as user_uid, tm.created, tm.updated, tm.permission
SELECT tm.id as id, t.uid as team_uid, t.id as team_id, u.uid as user_uid, u.id as user_id, tm.created, tm.updated, tm.permission
FROM "grafana"."team_member" tm
INNER JOIN "grafana"."team" t ON tm.team_id = t.id
INNER JOIN "grafana"."user" u ON tm.user_id = u.id

View File

@ -1,4 +1,4 @@
SELECT tm.id as id, t.uid as team_uid, t.id as team_id, u.uid as user_uid, tm.created, tm.updated, tm.permission
SELECT tm.id as id, t.uid as team_uid, t.id as team_id, u.uid as user_uid, u.id as user_id, tm.created, tm.updated, tm.permission
FROM "grafana"."team_member" tm
INNER JOIN "grafana"."team" t ON tm.team_id = t.id
INNER JOIN "grafana"."user" u ON tm.user_id = u.id

View File

@ -4,6 +4,7 @@ import (
"context"
"fmt"
"strconv"
"strings"
"time"
apierrors "k8s.io/apimachinery/pkg/api/errors"
@ -99,6 +100,12 @@ func (l *LegacyBindingStore) Create(ctx context.Context, obj runtime.Object, cre
return nil, fmt.Errorf("expected TeamBinding object, got %T", obj)
}
if createValidation != nil {
if err := createValidation(ctx, teamMemberObj); err != nil {
return nil, err
}
}
// Fetch the user by ID
userObj, err := l.store.GetUserInternalID(ctx, ns, legacy.GetUserInternalIDQuery{
UID: teamMemberObj.Spec.Subject.Name,
@ -115,12 +122,6 @@ func (l *LegacyBindingStore) Create(ctx context.Context, obj runtime.Object, cre
return nil, fmt.Errorf("failed to fetch team by id %s: %w", teamMemberObj.Spec.TeamRef.Name, err)
}
if createValidation != nil {
if err := createValidation(ctx, obj); err != nil {
return nil, err
}
}
var permission team.PermissionType
switch teamMemberObj.Spec.Permission {
case iamv0alpha1.TeamBindingTeamPermissionAdmin:
@ -131,7 +132,9 @@ func (l *LegacyBindingStore) Create(ctx context.Context, obj runtime.Object, cre
createCmd := legacy.CreateTeamMemberCommand{
TeamID: teamObj.ID,
TeamUID: teamMemberObj.Spec.TeamRef.Name,
UserID: userObj.ID,
UserUID: teamMemberObj.Spec.Subject.Name,
Permission: permission,
External: false,
}
@ -152,8 +155,11 @@ func (l *LegacyBindingStore) Get(ctx context.Context, name string, options *meta
return nil, err
}
teamID, userID := mapFromBindingName(name)
res, err := l.store.ListTeamBindings(ctx, ns, legacy.ListTeamBindingsQuery{
UID: name,
TeamID: teamID,
UserID: userID,
Pagination: common.Pagination{Limit: 1},
})
if err != nil {
@ -210,7 +216,7 @@ func mapToBindingObject(ns claims.NamespaceInfo, tm legacy.TeamMember) iamv0alph
return iamv0alpha1.TeamBinding{
ObjectMeta: metav1.ObjectMeta{
Name: tm.TeamUID,
Name: mapToBindingName(tm.TeamID, tm.UserID),
Namespace: ns.Value,
ResourceVersion: strconv.FormatInt(rv.UnixMilli(), 10),
CreationTimestamp: metav1.NewTime(ct),
@ -227,6 +233,33 @@ func mapToBindingObject(ns claims.NamespaceInfo, tm legacy.TeamMember) iamv0alph
}
}
func mapToBindingName(teamID int64, userID int64) string {
return fmt.Sprintf("binding-%d-%d", teamID, userID)
}
func mapFromBindingName(name string) (int64, int64) {
parts := strings.Split(name, "-")
if len(parts) != 3 {
return 0, 0
}
if parts[0] != "binding" {
return 0, 0
}
teamID, err := strconv.ParseInt(parts[1], 10, 64)
if err != nil {
return 0, 0
}
userID, err := strconv.ParseInt(parts[2], 10, 64)
if err != nil {
return 0, 0
}
return teamID, userID
}
func mapPermisson(p team.PermissionType) iamv0.TeamPermission {
if p == team.PermissionTypeAdmin {
return iamv0.TeamPermissionAdmin

View File

@ -57,9 +57,22 @@ func ValidateOnUpdate(ctx context.Context, obj, old *iamv0alpha1.Team) error {
}
func ValidateOnBindingCreate(ctx context.Context, obj *iamv0alpha1.TeamBinding) error {
_, err := identity.GetRequester(ctx)
if err != nil {
return apierrors.NewUnauthorized("no identity found")
}
if obj.Spec.Permission != iamv0alpha1.TeamBindingTeamPermissionAdmin && obj.Spec.Permission != iamv0alpha1.TeamBindingTeamPermissionMember {
return apierrors.NewBadRequest("invalid permission")
}
if obj.Spec.Subject.Name == "" {
return apierrors.NewBadRequest("subject is required")
}
if obj.Spec.TeamRef.Name == "" {
return apierrors.NewBadRequest("teamRef is required")
}
return nil
}

View File

@ -371,6 +371,60 @@ func TestValidateOnBindingCreate(t *testing.T) {
},
want: apierrors.NewBadRequest("invalid permission"),
},
{
name: "invalid team binding - no subject",
requester: &identity.StaticRequester{
Type: types.TypeUser,
OrgRole: identity.RoleAdmin,
},
obj: &iamv0alpha1.TeamBinding{
Spec: iamv0alpha1.TeamBindingSpec{
Subject: iamv0alpha1.TeamBindingspecSubject{
Name: "",
},
TeamRef: iamv0alpha1.TeamBindingTeamRef{
Name: "test-team",
},
Permission: iamv0alpha1.TeamBindingTeamPermissionAdmin,
},
},
want: apierrors.NewBadRequest("subject is required"),
},
{
name: "invalid team binding - no teamRef",
requester: &identity.StaticRequester{
Type: types.TypeUser,
OrgRole: identity.RoleAdmin,
},
obj: &iamv0alpha1.TeamBinding{
Spec: iamv0alpha1.TeamBindingSpec{
Subject: iamv0alpha1.TeamBindingspecSubject{
Name: "test-user",
},
TeamRef: iamv0alpha1.TeamBindingTeamRef{
Name: "",
},
Permission: iamv0alpha1.TeamBindingTeamPermissionAdmin,
},
},
want: apierrors.NewBadRequest("teamRef is required"),
},
{
name: "invalid team binding - no requester in context",
requester: nil,
obj: &iamv0alpha1.TeamBinding{
Spec: iamv0alpha1.TeamBindingSpec{
Subject: iamv0alpha1.TeamBindingspecSubject{
Name: "test-user",
},
TeamRef: iamv0alpha1.TeamBindingTeamRef{
Name: "test-team",
},
Permission: iamv0alpha1.TeamBindingTeamPermissionAdmin,
},
},
want: apierrors.NewUnauthorized("no identity found"),
},
}
for _, test := range tests {

View File

@ -27,6 +27,12 @@ var gvrUsers = schema.GroupVersionResource{
Resource: "users",
}
var gvrTeamBindings = schema.GroupVersionResource{
Group: "iam.grafana.app",
Version: "v0alpha1",
Resource: "teambindings",
}
func TestMain(m *testing.M) {
testsuite.Run(m)
}

View File

@ -0,0 +1,295 @@
package identity
import (
"context"
"fmt"
"testing"
"github.com/stretchr/testify/require"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"github.com/grafana/grafana/pkg/apiserver/rest"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/tests/apis"
"github.com/grafana/grafana/pkg/tests/testinfra"
"github.com/grafana/grafana/pkg/util/testutil"
)
func TestIntegrationTeamBindings(t *testing.T) {
testutil.SkipIntegrationTestInShortMode(t)
// TODO: Add rest.Mode4 when it's supported
modes := []rest.DualWriterMode{rest.Mode0, rest.Mode1, rest.Mode2, rest.Mode3}
for _, mode := range modes {
t.Run(fmt.Sprintf("Team binding CRUD operations with dual writer mode %d", mode), func(t *testing.T) {
helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{
AppModeProduction: false,
DisableAnonymous: true,
APIServerStorageType: "unified",
UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{
"teambindings.iam.grafana.app": {
DualWriterMode: mode,
},
},
EnableFeatureToggles: []string{
featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs,
featuremgmt.FlagKubernetesAuthnMutation,
},
})
ctx := context.Background()
// Create a team
teamClient := helper.GetResourceClient(apis.ResourceClientArgs{
User: helper.Org1.Admin,
Namespace: helper.Namespacer(helper.Org1.Admin.Identity.GetOrgID()),
GVR: gvrTeams,
})
team, err := teamClient.Resource.Create(ctx, helper.LoadYAMLOrJSONFile("testdata/team-test-create-v0.yaml"), metav1.CreateOptions{})
require.NoError(t, err)
require.NotNil(t, team)
// Create a user
userClient := helper.GetResourceClient(apis.ResourceClientArgs{
User: helper.Org1.Admin,
Namespace: helper.Namespacer(helper.Org1.Admin.Identity.GetOrgID()),
GVR: gvrUsers,
})
user, err := userClient.Resource.Create(ctx, helper.LoadYAMLOrJSONFile("testdata/user-test-create-v0.yaml"), metav1.CreateOptions{})
require.NoError(t, err)
require.NotNil(t, user)
doTeamBindingCRUDTestsUsingTheNewAPIs(t, helper, team, user)
if mode < 3 {
doTeamBindingCRUDTestsUsingTheLegacyAPIs(t, helper, mode)
}
})
}
}
func doTeamBindingCRUDTestsUsingTheNewAPIs(t *testing.T, helper *apis.K8sTestHelper, team *unstructured.Unstructured, user *unstructured.Unstructured) {
t.Run("should create/get team binding using the new APIs", func(t *testing.T) {
ctx := context.Background()
teamBindingClient := helper.GetResourceClient(apis.ResourceClientArgs{
User: helper.Org1.Admin,
Namespace: helper.Namespacer(helper.Org1.Admin.Identity.GetOrgID()),
GVR: gvrTeamBindings,
})
// Create the team binding
toCreate := helper.LoadYAMLOrJSONFile("testdata/teambinding-test-create-v0.yaml")
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)
require.NotNil(t, created)
createdSpec := created.Object["spec"].(map[string]interface{})
require.Equal(t, user.GetName(), createdSpec["subject"].(map[string]interface{})["name"])
require.Equal(t, team.GetName(), createdSpec["teamRef"].(map[string]interface{})["name"])
require.Equal(t, "admin", createdSpec["permission"])
createdUID := created.GetName()
require.NotEmpty(t, createdUID)
// Get the team binding
fetched, err := teamBindingClient.Resource.Get(ctx, createdUID, metav1.GetOptions{})
require.NoError(t, err)
require.NotNil(t, fetched)
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, createdUID, fetched.GetName())
require.Equal(t, "default", fetched.GetNamespace())
})
t.Run("should not be able to create team binding when using a user with insufficient permissions", func(t *testing.T) {
for _, u := range []apis.User{
helper.Org1.Editor,
helper.Org1.Viewer,
} {
t.Run(fmt.Sprintf("with basic role_%s", u.Identity.GetOrgRole()), func(t *testing.T) {
ctx := context.Background()
teamBindingClient := helper.GetResourceClient(apis.ResourceClientArgs{
User: u,
Namespace: helper.Namespacer(helper.Org1.Admin.Identity.GetOrgID()),
GVR: gvrTeamBindings,
})
toCreate := helper.LoadYAMLOrJSONFile("testdata/teambinding-test-create-v0.yaml")
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()
_, err := teamBindingClient.Resource.Create(ctx, toCreate, metav1.CreateOptions{})
require.Error(t, err)
var statusErr *errors.StatusError
require.ErrorAs(t, err, &statusErr)
require.Equal(t, int32(403), statusErr.ErrStatus.Code)
})
}
})
t.Run("should not be able to create team binding without a subject", func(t *testing.T) {
ctx := context.Background()
teamBindingClient := helper.GetResourceClient(apis.ResourceClientArgs{
User: helper.Org1.Admin,
Namespace: helper.Namespacer(helper.Org1.Admin.Identity.GetOrgID()),
GVR: gvrTeamBindings,
})
toCreate := helper.LoadYAMLOrJSONFile("testdata/teambinding-test-create-v0.yaml")
toCreate.Object["spec"].(map[string]interface{})["subject"].(map[string]interface{})["name"] = ""
toCreate.Object["spec"].(map[string]interface{})["teamRef"].(map[string]interface{})["name"] = team.GetName()
_, err := teamBindingClient.Resource.Create(ctx, toCreate, metav1.CreateOptions{})
require.Error(t, err)
var statusErr *errors.StatusError
require.ErrorAs(t, err, &statusErr)
require.Equal(t, int32(400), statusErr.ErrStatus.Code)
require.Contains(t, statusErr.ErrStatus.Message, "subject is required")
})
t.Run("should not be able to create team binding without a teamRef", func(t *testing.T) {
ctx := context.Background()
teamBindingClient := helper.GetResourceClient(apis.ResourceClientArgs{
User: helper.Org1.Admin,
Namespace: helper.Namespacer(helper.Org1.Admin.Identity.GetOrgID()),
GVR: gvrTeamBindings,
})
toCreate := helper.LoadYAMLOrJSONFile("testdata/teambinding-test-create-v0.yaml")
toCreate.Object["spec"].(map[string]interface{})["subject"].(map[string]interface{})["name"] = user.GetName()
toCreate.Object["spec"].(map[string]interface{})["teamRef"].(map[string]interface{})["name"] = ""
_, err := teamBindingClient.Resource.Create(ctx, toCreate, metav1.CreateOptions{})
require.Error(t, err)
var statusErr *errors.StatusError
require.ErrorAs(t, err, &statusErr)
require.Equal(t, int32(400), statusErr.ErrStatus.Code)
require.Contains(t, statusErr.ErrStatus.Message, "teamRef is required")
})
t.Run("should not be able to create team binding with invalid permission", func(t *testing.T) {
ctx := context.Background()
teamBindingClient := helper.GetResourceClient(apis.ResourceClientArgs{
User: helper.Org1.Admin,
Namespace: helper.Namespacer(helper.Org1.Admin.Identity.GetOrgID()),
GVR: gvrTeamBindings,
})
toCreate := helper.LoadYAMLOrJSONFile("testdata/teambinding-test-create-v0.yaml")
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()
toCreate.Object["spec"].(map[string]interface{})["permission"] = "invalid"
_, err := teamBindingClient.Resource.Create(ctx, toCreate, metav1.CreateOptions{})
require.Error(t, err)
var statusErr *errors.StatusError
require.ErrorAs(t, err, &statusErr)
require.Equal(t, int32(400), statusErr.ErrStatus.Code)
require.Contains(t, statusErr.ErrStatus.Message, "invalid permission")
})
}
func doTeamBindingCRUDTestsUsingTheLegacyAPIs(t *testing.T, helper *apis.K8sTestHelper, mode rest.DualWriterMode) {
t.Run("should create team binding using legacy APIs and get it using the new APIs", func(t *testing.T) {
ctx := context.Background()
// Create a team using legacy API
legacyTeamPayload := `{
"name": "Test Team Legacy",
"email": "testteamlegacy@example.com"
}`
type legacyTeamResponse struct {
UID string `json:"uid"`
ID int64 `json:"teamId"`
}
teamRsp := apis.DoRequest(helper, apis.RequestParams{
User: helper.Org1.Admin,
Method: "POST",
Path: "/api/teams",
Body: []byte(legacyTeamPayload),
}, &legacyTeamResponse{})
require.NotNil(t, teamRsp)
require.Equal(t, 200, teamRsp.Response.StatusCode)
require.NotEmpty(t, teamRsp.Result.UID)
// Create a user using legacy API
legacyUserPayload := `{
"name": "Test User 2",
"email": "testuser2@example.com",
"login": "testuser2",
"password": "password123"
}`
type legacyUserResponse struct {
UID string `json:"uid"`
ID int64 `json:"id"`
}
userRsp := apis.DoRequest(helper, apis.RequestParams{
User: helper.Org1.Admin,
Method: "POST",
Path: "/api/admin/users",
Body: []byte(legacyUserPayload),
}, &legacyUserResponse{})
require.NotNil(t, userRsp)
require.Equal(t, 200, userRsp.Response.StatusCode)
require.NotEmpty(t, userRsp.Result.UID)
// Create team binding using legacy API
legacyTeamBindingPayload := `{
"userId": ` + fmt.Sprintf("%d", userRsp.Result.ID) + `,
"teamId": ` + fmt.Sprintf("%d", teamRsp.Result.ID) + `,
"permission": "member"
}`
type legacyTeamBindingResponse struct {
UserID int64 `json:"userId"`
TeamID int64 `json:"teamId"`
Permission string `json:"permission"`
}
teamBindingRsp := apis.DoRequest(helper, apis.RequestParams{
User: helper.Org1.Admin,
Method: "POST",
Path: "/api/teams/" + teamRsp.Result.UID + "/members",
Body: []byte(legacyTeamBindingPayload),
}, &legacyTeamBindingResponse{})
require.NotNil(t, teamBindingRsp)
require.Equal(t, 200, teamBindingRsp.Response.StatusCode)
// Get team binding using new API
teamBindingClient := helper.GetResourceClient(apis.ResourceClientArgs{
User: helper.Org1.Admin,
Namespace: helper.Namespacer(helper.Org1.Admin.Identity.GetOrgID()),
GVR: gvrTeamBindings,
})
teamBindingName := fmt.Sprintf("binding-%d-%d", teamRsp.Result.ID, userRsp.Result.ID)
teamBinding, err := teamBindingClient.Resource.Get(ctx, teamBindingName, metav1.GetOptions{})
require.NoError(t, err)
require.NotNil(t, teamBinding)
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())
})
}

View File

@ -0,0 +1,10 @@
apiVersion: iam.grafana.app/v0alpha1
kind: TeamBinding
metadata:
name: test-team-binding-1
spec:
subject:
name: ""
teamRef:
name: ""
permission: "admin"