Compare commits

..

2 Commits

Author SHA1 Message Date
Mihai Doarna e189d8547f fix validation test
CodeQL checks / Detect whether code changed (push) Has been cancelled Details
CodeQL checks / Analyze (actions) (push) Has been cancelled Details
CodeQL checks / Analyze (go) (push) Has been cancelled Details
CodeQL checks / Analyze (javascript) (push) Has been cancelled 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 // Identity specific resources
legacyAuthorizer := gfauthorizer.NewResourceAuthorizer(legacyAccessClient) legacyAuthorizer := gfauthorizer.NewResourceAuthorizer(legacyAccessClient)
resourceAuthorizer[iamv0.TeamResourceInfo.GetName()] = legacyAuthorizer resourceAuthorizer[iamv0.TeamResourceInfo.GetName()] = legacyAuthorizer
resourceAuthorizer[iamv0.TeamBindingResourceInfo.GetName()] = legacyAuthorizer
resourceAuthorizer["display"] = legacyAuthorizer resourceAuthorizer["display"] = legacyAuthorizer
// Access specific resources // Access specific resources

View File

@ -1,5 +1,5 @@
INSERT INTO {{ .Ident .TeamMemberTable }} INSERT INTO {{ .Ident .TeamMemberTable }}
(team_id, user_id, created, updated, external, permission) (team_id, user_id, org_id, created, updated, external, permission)
VALUES 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 }}) {{ .Arg .Command.Updated }}, {{ .Arg .Command.External }}, {{ .Arg .Command.Permission }})

View File

@ -226,7 +226,8 @@ func TestIdentityQueries(t *testing.T) {
Name: "team_1_bindings", Name: "team_1_bindings",
Data: listTeamBindings(&ListTeamBindingsQuery{ Data: listTeamBindings(&ListTeamBindingsQuery{
OrgID: 1, OrgID: 1,
UID: "team-1", TeamID: 1,
UserID: 1,
Pagination: common.Pagination{Limit: 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 { type ListTeamBindingsQuery struct {
// UID is team uid to list bindings for. If not set store should list bindings for all teams TeamID int64
UID string UserID int64
OrgID int64 OrgID int64
Pagination common.Pagination Pagination common.Pagination
} }
@ -432,6 +432,7 @@ type TeamMember struct {
TeamUID string TeamUID string
UserID int64 UserID int64
UserUID string UserUID string
OrgID int64
Name string Name string
Email string Email string
Username string Username string
@ -486,7 +487,7 @@ func (s *legacySQLStore) ListTeamBindings(ctx context.Context, ns claims.Namespa
req := newListTeamBindings(sql, &query) req := newListTeamBindings(sql, &query)
q, err := sqltemplate.Execute(sqlQueryTeamBindingsTemplate, req) q, err := sqltemplate.Execute(sqlQueryTeamBindingsTemplate, req)
if err != nil { 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()...) 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() { for rows.Next() {
m := TeamMember{} 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 { if err != nil {
return res, err 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 return res, err
} }
type CreateTeamMemberCommand struct { type CreateTeamMemberCommand struct {
TeamID int64 TeamID int64
TeamUID string
UserID int64 UserID int64
UserUID string
OrgID int64
Created DBTime Created DBTime
Updated DBTime Updated DBTime
External bool External bool
@ -566,6 +566,11 @@ func (s *legacySQLStore) CreateTeamMember(ctx context.Context, ns claims.Namespa
now := time.Now().UTC().Truncate(time.Second) now := time.Now().UTC().Truncate(time.Second)
cmd.Created = NewDBTime(now) cmd.Created = NewDBTime(now)
cmd.Updated = 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) sql, err := s.sql(ctx)
if err != nil { if err != nil {
@ -589,7 +594,10 @@ func (s *legacySQLStore) CreateTeamMember(ctx context.Context, ns claims.Namespa
createdTeamMember = TeamMember{ createdTeamMember = TeamMember{
ID: teamMemberID, ID: teamMemberID,
TeamID: cmd.TeamID, TeamID: cmd.TeamID,
TeamUID: cmd.TeamUID,
UserID: cmd.UserID, UserID: cmd.UserID,
UserUID: cmd.UserUID,
OrgID: cmd.OrgID,
Created: cmd.Created.Time, Created: cmd.Created.Time,
Updated: cmd.Updated.Time, Updated: cmd.Updated.Time,
External: cmd.External, 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 FROM {{ .Ident .TeamMemberTable }} tm
INNER JOIN {{ .Ident .TeamTable }} t ON tm.team_id = t.id INNER JOIN {{ .Ident .TeamTable }} t ON tm.team_id = t.id
INNER JOIN {{ .Ident .UserTable }} u ON tm.user_id = u.id INNER JOIN {{ .Ident .UserTable }} u ON tm.user_id = u.id
WHERE WHERE
tm.org_id = {{ .Arg .Query.OrgID}} tm.org_id = {{ .Arg .Query.OrgID}}
{{ if .Query.UID }} {{ if .Query.TeamID }}
AND t.uid = {{ .Arg .Query.UID }} AND tm.team_id = {{ .Arg .Query.TeamID }}
{{ end }}
{{ if .Query.UserID }}
AND tm.user_id = {{ .Arg .Query.UserID }}
{{ end }} {{ end }}
{{- if .Query.Pagination.Continue }} {{- if .Query.Pagination.Continue }}
AND tm.id >= {{ .Arg .Query.Pagination.Continue }} AND tm.id >= {{ .Arg .Query.Pagination.Continue }}

View File

@ -1,5 +1,5 @@
INSERT INTO `grafana`.`team_member` INSERT INTO `grafana`.`team_member`
(team_id, user_id, created, updated, external, permission) (team_id, user_id, org_id, created, updated, external, permission)
VALUES 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') '2023-01-01 12:00:00', FALSE, 'Member')

View File

@ -1,5 +1,5 @@
INSERT INTO `grafana`.`team_member` INSERT INTO `grafana`.`team_member`
(team_id, user_id, created, updated, external, permission) (team_id, user_id, org_id, created, updated, external, permission)
VALUES 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') '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 FROM `grafana`.`team_member` tm
INNER JOIN `grafana`.`team` t ON tm.team_id = t.id INNER JOIN `grafana`.`team` t ON tm.team_id = t.id
INNER JOIN `grafana`.`user` u ON tm.user_id = u.id INNER JOIN `grafana`.`user` u ON tm.user_id = u.id
WHERE WHERE
tm.org_id = 1 tm.org_id = 1
AND t.uid = 'team-1' AND tm.team_id = 1
AND tm.user_id = 1
AND NOT tm.external AND NOT tm.external
ORDER BY t.id ASC ORDER BY t.id ASC
LIMIT 1; 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 FROM `grafana`.`team_member` tm
INNER JOIN `grafana`.`team` t ON tm.team_id = t.id INNER JOIN `grafana`.`team` t ON tm.team_id = t.id
INNER JOIN `grafana`.`user` u ON tm.user_id = u.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 FROM `grafana`.`team_member` tm
INNER JOIN `grafana`.`team` t ON tm.team_id = t.id INNER JOIN `grafana`.`team` t ON tm.team_id = t.id
INNER JOIN `grafana`.`user` u ON tm.user_id = u.id INNER JOIN `grafana`.`user` u ON tm.user_id = u.id

View File

@ -1,5 +1,5 @@
INSERT INTO "grafana"."team_member" INSERT INTO "grafana"."team_member"
(team_id, user_id, created, updated, external, permission) (team_id, user_id, org_id, created, updated, external, permission)
VALUES 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') '2023-01-01 12:00:00', FALSE, 'Member')

View File

@ -1,5 +1,5 @@
INSERT INTO "grafana"."team_member" INSERT INTO "grafana"."team_member"
(team_id, user_id, created, updated, external, permission) (team_id, user_id, org_id, created, updated, external, permission)
VALUES 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') '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 FROM "grafana"."team_member" tm
INNER JOIN "grafana"."team" t ON tm.team_id = t.id INNER JOIN "grafana"."team" t ON tm.team_id = t.id
INNER JOIN "grafana"."user" u ON tm.user_id = u.id INNER JOIN "grafana"."user" u ON tm.user_id = u.id
WHERE WHERE
tm.org_id = 1 tm.org_id = 1
AND t.uid = 'team-1' AND tm.team_id = 1
AND tm.user_id = 1
AND NOT tm.external AND NOT tm.external
ORDER BY t.id ASC ORDER BY t.id ASC
LIMIT 1; 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 FROM "grafana"."team_member" tm
INNER JOIN "grafana"."team" t ON tm.team_id = t.id INNER JOIN "grafana"."team" t ON tm.team_id = t.id
INNER JOIN "grafana"."user" u ON tm.user_id = u.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 FROM "grafana"."team_member" tm
INNER JOIN "grafana"."team" t ON tm.team_id = t.id INNER JOIN "grafana"."team" t ON tm.team_id = t.id
INNER JOIN "grafana"."user" u ON tm.user_id = u.id INNER JOIN "grafana"."user" u ON tm.user_id = u.id

View File

@ -1,5 +1,5 @@
INSERT INTO "grafana"."team_member" INSERT INTO "grafana"."team_member"
(team_id, user_id, created, updated, external, permission) (team_id, user_id, org_id, created, updated, external, permission)
VALUES 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') '2023-01-01 12:00:00', FALSE, 'Member')

View File

@ -1,5 +1,5 @@
INSERT INTO "grafana"."team_member" INSERT INTO "grafana"."team_member"
(team_id, user_id, created, updated, external, permission) (team_id, user_id, org_id, created, updated, external, permission)
VALUES 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') '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 FROM "grafana"."team_member" tm
INNER JOIN "grafana"."team" t ON tm.team_id = t.id INNER JOIN "grafana"."team" t ON tm.team_id = t.id
INNER JOIN "grafana"."user" u ON tm.user_id = u.id INNER JOIN "grafana"."user" u ON tm.user_id = u.id
WHERE WHERE
tm.org_id = 1 tm.org_id = 1
AND t.uid = 'team-1' AND tm.team_id = 1
AND tm.user_id = 1
AND NOT tm.external AND NOT tm.external
ORDER BY t.id ASC ORDER BY t.id ASC
LIMIT 1; 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 FROM "grafana"."team_member" tm
INNER JOIN "grafana"."team" t ON tm.team_id = t.id INNER JOIN "grafana"."team" t ON tm.team_id = t.id
INNER JOIN "grafana"."user" u ON tm.user_id = u.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 FROM "grafana"."team_member" tm
INNER JOIN "grafana"."team" t ON tm.team_id = t.id INNER JOIN "grafana"."team" t ON tm.team_id = t.id
INNER JOIN "grafana"."user" u ON tm.user_id = u.id INNER JOIN "grafana"."user" u ON tm.user_id = u.id

View File

@ -4,6 +4,7 @@ import (
"context" "context"
"fmt" "fmt"
"strconv" "strconv"
"strings"
"time" "time"
apierrors "k8s.io/apimachinery/pkg/api/errors" 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) 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 // Fetch the user by ID
userObj, err := l.store.GetUserInternalID(ctx, ns, legacy.GetUserInternalIDQuery{ userObj, err := l.store.GetUserInternalID(ctx, ns, legacy.GetUserInternalIDQuery{
UID: teamMemberObj.Spec.Subject.Name, 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) 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 var permission team.PermissionType
switch teamMemberObj.Spec.Permission { switch teamMemberObj.Spec.Permission {
case iamv0alpha1.TeamBindingTeamPermissionAdmin: case iamv0alpha1.TeamBindingTeamPermissionAdmin:
@ -131,7 +132,9 @@ func (l *LegacyBindingStore) Create(ctx context.Context, obj runtime.Object, cre
createCmd := legacy.CreateTeamMemberCommand{ createCmd := legacy.CreateTeamMemberCommand{
TeamID: teamObj.ID, TeamID: teamObj.ID,
TeamUID: teamMemberObj.Spec.TeamRef.Name,
UserID: userObj.ID, UserID: userObj.ID,
UserUID: teamMemberObj.Spec.Subject.Name,
Permission: permission, Permission: permission,
External: false, External: false,
} }
@ -152,8 +155,11 @@ func (l *LegacyBindingStore) Get(ctx context.Context, name string, options *meta
return nil, err return nil, err
} }
teamID, userID := mapFromBindingName(name)
res, err := l.store.ListTeamBindings(ctx, ns, legacy.ListTeamBindingsQuery{ res, err := l.store.ListTeamBindings(ctx, ns, legacy.ListTeamBindingsQuery{
UID: name, TeamID: teamID,
UserID: userID,
Pagination: common.Pagination{Limit: 1}, Pagination: common.Pagination{Limit: 1},
}) })
if err != nil { if err != nil {
@ -210,7 +216,7 @@ func mapToBindingObject(ns claims.NamespaceInfo, tm legacy.TeamMember) iamv0alph
return iamv0alpha1.TeamBinding{ return iamv0alpha1.TeamBinding{
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
Name: tm.TeamUID, Name: mapToBindingName(tm.TeamID, tm.UserID),
Namespace: ns.Value, Namespace: ns.Value,
ResourceVersion: strconv.FormatInt(rv.UnixMilli(), 10), ResourceVersion: strconv.FormatInt(rv.UnixMilli(), 10),
CreationTimestamp: metav1.NewTime(ct), 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 { func mapPermisson(p team.PermissionType) iamv0.TeamPermission {
if p == team.PermissionTypeAdmin { if p == team.PermissionTypeAdmin {
return iamv0.TeamPermissionAdmin 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 { 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 { if obj.Spec.Permission != iamv0alpha1.TeamBindingTeamPermissionAdmin && obj.Spec.Permission != iamv0alpha1.TeamBindingTeamPermissionMember {
return apierrors.NewBadRequest("invalid permission") 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 return nil
} }

View File

@ -371,6 +371,60 @@ func TestValidateOnBindingCreate(t *testing.T) {
}, },
want: apierrors.NewBadRequest("invalid permission"), 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 { for _, test := range tests {

View File

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