IAM: Add role to User (#111256)

* codegen fix

* Return user role from the legacy store

* Lint

* Add tests, gen openapi

* make generate

* revert go.mod, go.sum, go.work.sum changes

* Update go.mod and go.sum
This commit is contained in:
Misi
2025-09-19 11:12:00 +02:00
committed by GitHub
parent f48efa1003
commit c012a95fa1
39 changed files with 1485 additions and 274 deletions
-3
View File
@@ -14,9 +14,6 @@ func MutateOnCreate(ctx context.Context, obj *iamv0alpha1.User) error {
if obj.Spec.Login == "" {
obj.Spec.Login = obj.Spec.Email
}
if obj.Spec.Email == "" {
obj.Spec.Email = obj.Spec.Login
}
return nil
}
@@ -44,7 +44,6 @@ func TestMutateOnCreate_LoginEmail(t *testing.T) {
},
},
expectedLogin: "only.login",
expectedEmail: "only.login",
},
{
name: "login and email already lowercase",
+9 -6
View File
@@ -16,7 +16,7 @@ import (
"github.com/grafana/grafana/pkg/registry/apis/iam/common"
"github.com/grafana/grafana/pkg/registry/apis/iam/legacy"
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/util"
apierrors "k8s.io/apimachinery/pkg/api/errors"
)
@@ -196,16 +196,17 @@ func (s *LegacyStore) Create(ctx context.Context, obj runtime.Object, createVali
return nil, fmt.Errorf("expected User object, got %T", obj)
}
if userObj.GenerateName != "" {
userObj.Name = userObj.GenerateName + util.GenerateShortUID()
userObj.GenerateName = ""
}
if createValidation != nil {
if err := createValidation(ctx, obj); err != nil {
return nil, err
}
}
if userObj.Spec.Login == "" && userObj.Spec.Email == "" {
return nil, fmt.Errorf("user must have either login or email")
}
createCmd := legacy.CreateUserCommand{
UID: userObj.Name,
Login: userObj.Spec.Login,
@@ -215,6 +216,7 @@ func (s *LegacyStore) Create(ctx context.Context, obj runtime.Object, createVali
IsDisabled: userObj.Spec.Disabled,
EmailVerified: userObj.Spec.EmailVerified,
IsProvisioned: userObj.Spec.Provisioned,
Role: userObj.Spec.Role,
}
result, err := s.store.CreateUser(ctx, ns, createCmd)
@@ -226,7 +228,7 @@ func (s *LegacyStore) Create(ctx context.Context, obj runtime.Object, createVali
return &iamUser, nil
}
func toUserItem(u *user.User, ns string) iamv0alpha1.User {
func toUserItem(u *common.UserWithRole, ns string) iamv0alpha1.User {
item := &iamv0alpha1.User{
ObjectMeta: metav1.ObjectMeta{
Name: u.UID,
@@ -242,6 +244,7 @@ func toUserItem(u *user.User, ns string) iamv0alpha1.User {
Disabled: u.IsDisabled,
GrafanaAdmin: u.IsAdmin,
Provisioned: u.IsProvisioned,
Role: u.Role,
},
}
obj, _ := utils.MetaAccessor(item)
+31
View File
@@ -0,0 +1,31 @@
package user
import (
"context"
"fmt"
apierrors "k8s.io/apimachinery/pkg/api/errors"
iamv0alpha1 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1"
"github.com/grafana/grafana/pkg/apimachinery/identity"
)
func ValidateOnCreate(ctx context.Context, obj *iamv0alpha1.User) error {
requester, err := identity.GetRequester(ctx)
if err != nil {
return apierrors.NewUnauthorized("no identity found")
}
// Temporary validation that the user is not trying to create a Grafana Admin without being a Grafana Admin.
if obj.Spec.GrafanaAdmin && !requester.GetIsGrafanaAdmin() {
return apierrors.NewForbidden(iamv0alpha1.UserResourceInfo.GroupResource(),
obj.Name,
fmt.Errorf("only grafana admins can create grafana admins"))
}
if obj.Spec.Login == "" && obj.Spec.Email == "" {
return apierrors.NewBadRequest("user must have either login or email")
}
return nil
}
+123
View File
@@ -0,0 +1,123 @@
package user
import (
"context"
"testing"
"github.com/stretchr/testify/require"
"github.com/grafana/authlib/types"
iamv0alpha1 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1"
"github.com/grafana/grafana/pkg/apimachinery/identity"
)
func TestValidateOnCreate(t *testing.T) {
tests := []struct {
name string
user *iamv0alpha1.User
requester *identity.StaticRequester
expectError bool
errorContains string
}{
{
name: "valid user creation by grafana admin",
user: &iamv0alpha1.User{
Spec: iamv0alpha1.UserSpec{
Login: "testuser",
},
},
requester: &identity.StaticRequester{
Type: types.TypeUser,
IsGrafanaAdmin: true,
},
expectError: false,
},
{
name: "grafana admin creating another grafana admin",
user: &iamv0alpha1.User{
Spec: iamv0alpha1.UserSpec{
Login: "newadmin",
GrafanaAdmin: true,
},
},
requester: &identity.StaticRequester{
Type: types.TypeUser,
IsGrafanaAdmin: true,
},
expectError: false,
},
{
name: "non-admin trying to create a grafana admin",
user: &iamv0alpha1.User{
Spec: iamv0alpha1.UserSpec{
Login: "newadmin",
GrafanaAdmin: true,
},
},
requester: &identity.StaticRequester{
Type: types.TypeUser,
IsGrafanaAdmin: false,
},
expectError: true,
errorContains: "only grafana admins can create grafana admins",
},
{
name: "user with empty login and email",
user: &iamv0alpha1.User{
Spec: iamv0alpha1.UserSpec{},
},
requester: &identity.StaticRequester{
Type: types.TypeUser,
IsGrafanaAdmin: false,
},
expectError: true,
errorContains: "user must have either login or email",
},
{
name: "user with only login",
user: &iamv0alpha1.User{
Spec: iamv0alpha1.UserSpec{
Login: "testuser",
},
},
requester: &identity.StaticRequester{
Type: types.TypeUser,
IsGrafanaAdmin: false,
},
expectError: false,
},
{
name: "user with only email",
user: &iamv0alpha1.User{
Spec: iamv0alpha1.UserSpec{
Email: "test@test.com",
},
},
requester: &identity.StaticRequester{
Type: types.TypeUser,
IsGrafanaAdmin: false,
},
expectError: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctx := identity.WithRequester(
context.Background(),
tt.requester,
)
err := ValidateOnCreate(ctx, tt.user)
if tt.expectError {
require.Error(t, err)
if tt.errorContains != "" {
require.Contains(t, err.Error(), tt.errorContains)
}
} else {
require.NoError(t, err)
}
})
}
}