Files
grafana/pkg/registry/apis/iam/user/validate_test.go
Misi 0c264b7a5f IAM: Add user search endpoint (#114542)
* wip: initial changes, api registration

* wip

* LegacySearch working with sorting

* Revert mapper change for now

* Clean up

* Cleanup, add integration tests

* Improve tests

* OpenAPI def regen

* Use wildcard search, fix lastSeenAt handling, add lastSeenAtAge

* Add missing files

* Fix merge

* Fixes

* Add tests, regen openapi def

* Address feedback

* Address feedback batch 2

* Chores

* regen openapidef

* Address feedback

* Add tests for paging

* gen apis

* Revert go.mod, go.sum. go.work.sum

* Fix + remove extra tracer parameter
2025-12-11 19:54:48 +01:00

567 lines
15 KiB
Go

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"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/storage/unified/resourcepb"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
func TestValidateOnCreate(t *testing.T) {
tests := []struct {
name string
user *iamv0alpha1.User
requester *identity.StaticRequester
searchClient resourcepb.ResourceIndexClient
expectError bool
errorContains string
}{
{
name: "valid user creation by grafana admin",
user: &iamv0alpha1.User{
Spec: iamv0alpha1.UserSpec{
Login: "testuser",
Role: "Viewer",
},
},
requester: &identity.StaticRequester{
Type: types.TypeUser,
IsGrafanaAdmin: true,
},
searchClient: &FakeUserLegacySearchClient{},
expectError: false,
},
{
name: "grafana admin creating another grafana admin",
user: &iamv0alpha1.User{
Spec: iamv0alpha1.UserSpec{
Login: "newadmin",
GrafanaAdmin: true,
Role: "Viewer",
},
},
requester: &identity.StaticRequester{
Type: types.TypeUser,
IsGrafanaAdmin: true,
},
searchClient: &FakeUserLegacySearchClient{},
expectError: false,
},
{
name: "non-admin trying to create a grafana admin",
user: &iamv0alpha1.User{
Spec: iamv0alpha1.UserSpec{
Login: "newadmin",
GrafanaAdmin: true,
Role: "Viewer",
},
},
requester: &identity.StaticRequester{
Type: types.TypeUser,
IsGrafanaAdmin: false,
},
searchClient: &FakeUserLegacySearchClient{},
expectError: true,
errorContains: "only grafana admins can create grafana admins",
},
{
name: "user with empty login and email",
user: &iamv0alpha1.User{
Spec: iamv0alpha1.UserSpec{
Role: "Viewer",
},
},
requester: &identity.StaticRequester{
Type: types.TypeUser,
IsGrafanaAdmin: false,
},
searchClient: &FakeUserLegacySearchClient{},
expectError: true,
errorContains: "user must have either login or email",
},
{
name: "user with only login",
user: &iamv0alpha1.User{
Spec: iamv0alpha1.UserSpec{
Login: "testuser",
Role: "Viewer",
},
},
requester: &identity.StaticRequester{
Type: types.TypeUser,
IsGrafanaAdmin: false,
},
searchClient: &FakeUserLegacySearchClient{},
expectError: false,
},
{
name: "user with only email",
user: &iamv0alpha1.User{
Spec: iamv0alpha1.UserSpec{
Email: "test@example",
Role: "Viewer",
},
},
requester: &identity.StaticRequester{
Type: types.TypeUser,
IsGrafanaAdmin: false,
},
searchClient: &FakeUserLegacySearchClient{},
expectError: false,
},
{
name: "user with empty role",
user: &iamv0alpha1.User{
Spec: iamv0alpha1.UserSpec{
Login: "testuser",
},
},
requester: &identity.StaticRequester{
Type: types.TypeUser,
IsGrafanaAdmin: false,
},
searchClient: &FakeUserLegacySearchClient{},
expectError: true,
errorContains: "role is required",
},
{
name: "user with invalid role",
user: &iamv0alpha1.User{
Spec: iamv0alpha1.UserSpec{
Login: "testuser",
Role: "InvalidRole",
},
},
requester: &identity.StaticRequester{
Type: types.TypeUser,
IsGrafanaAdmin: false,
},
searchClient: &FakeUserLegacySearchClient{},
expectError: true,
errorContains: "invalid role 'InvalidRole'",
},
{
name: "user with valid role",
user: &iamv0alpha1.User{
Spec: iamv0alpha1.UserSpec{
Login: "testuser",
Role: "Admin",
},
},
requester: &identity.StaticRequester{
Type: types.TypeUser,
IsGrafanaAdmin: true,
},
searchClient: &FakeUserLegacySearchClient{},
expectError: false,
},
{
name: "user with existing email",
user: &iamv0alpha1.User{
ObjectMeta: metav1.ObjectMeta{
Name: "userx",
},
Spec: iamv0alpha1.UserSpec{
Email: "existing@example",
Role: "Viewer",
},
},
requester: &identity.StaticRequester{
Type: types.TypeUser,
IsGrafanaAdmin: false,
},
searchClient: &FakeUserLegacySearchClient{
Users: []*org.OrgUserDTO{
{Email: "existing@example"},
},
},
expectError: true,
errorContains: "email 'existing@example' is already taken",
},
{
name: "user with existing login",
user: &iamv0alpha1.User{
ObjectMeta: metav1.ObjectMeta{
Name: "userx",
},
Spec: iamv0alpha1.UserSpec{
Login: "existinguser",
Email: "existinguser@example",
Role: "Viewer",
},
},
requester: &identity.StaticRequester{
Type: types.TypeUser,
IsGrafanaAdmin: false,
},
searchClient: &FakeUserLegacySearchClient{
Users: []*org.OrgUserDTO{
{Login: "existinguser"},
},
},
expectError: true,
errorContains: "login 'existinguser' is already taken",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctx := identity.WithRequester(
context.Background(),
tt.requester,
)
err := ValidateOnCreate(ctx, tt.searchClient, tt.user)
if tt.expectError {
require.Error(t, err)
if tt.errorContains != "" {
require.Contains(t, err.Error(), tt.errorContains)
}
} else {
require.NoError(t, err)
}
})
}
}
func TestValidateOnUpdate(t *testing.T) {
tests := []struct {
name string
oldUser *iamv0alpha1.User
newUser *iamv0alpha1.User
requester *identity.StaticRequester
searchClient resourcepb.ResourceIndexClient
expectError bool
errorContains string
}{
{
name: "un-provisioning a provisioned user",
oldUser: &iamv0alpha1.User{
Spec: iamv0alpha1.UserSpec{Login: "testuser", Provisioned: true, Role: "Viewer"},
},
newUser: &iamv0alpha1.User{
Spec: iamv0alpha1.UserSpec{Login: "testuser", Provisioned: false, Role: "Viewer"},
},
requester: &identity.StaticRequester{
Type: types.TypeUser,
IsGrafanaAdmin: true,
},
expectError: true,
errorContains: "provisioned user cannot be un-provisioned",
},
{
name: "non-service user provisions a user",
oldUser: &iamv0alpha1.User{
Spec: iamv0alpha1.UserSpec{Login: "testuser", Provisioned: false, Role: "Viewer"},
},
newUser: &iamv0alpha1.User{
Spec: iamv0alpha1.UserSpec{Login: "testuser", Provisioned: true, Role: "Viewer"},
},
requester: &identity.StaticRequester{
Type: types.TypeUser,
IsGrafanaAdmin: true,
},
expectError: true,
errorContains: "only service users can provision a user",
},
{
name: "service user provisions a user",
oldUser: &iamv0alpha1.User{
Spec: iamv0alpha1.UserSpec{Login: "testuser", Provisioned: false, Role: "Viewer"},
},
newUser: &iamv0alpha1.User{
Spec: iamv0alpha1.UserSpec{Login: "testuser", Provisioned: true, Role: "Viewer"},
},
requester: &identity.StaticRequester{
Type: types.TypeAccessPolicy,
},
expectError: false,
},
{
name: "no changes",
oldUser: &iamv0alpha1.User{
Spec: iamv0alpha1.UserSpec{Login: "testuser", Role: "Viewer"},
},
newUser: &iamv0alpha1.User{
Spec: iamv0alpha1.UserSpec{Login: "testuser", Role: "Viewer"},
},
requester: &identity.StaticRequester{
Type: types.TypeUser,
},
expectError: false,
},
{
name: "update with empty login and email",
oldUser: &iamv0alpha1.User{
Spec: iamv0alpha1.UserSpec{Login: "testuser", Role: "Viewer"},
},
newUser: &iamv0alpha1.User{
Spec: iamv0alpha1.UserSpec{Login: "", Email: "", Role: "Viewer"},
},
requester: &identity.StaticRequester{
Type: types.TypeUser,
},
expectError: true,
errorContains: "user must have either login or email",
},
{
name: "update with only login",
oldUser: &iamv0alpha1.User{
Spec: iamv0alpha1.UserSpec{Email: "test@example", Role: "Viewer"},
},
newUser: &iamv0alpha1.User{
Spec: iamv0alpha1.UserSpec{Login: "testuser", Email: "", Role: "Viewer"},
},
requester: &identity.StaticRequester{
Type: types.TypeUser,
IsGrafanaAdmin: true,
},
searchClient: &FakeUserLegacySearchClient{},
expectError: false,
},
{
name: "update with only email",
oldUser: &iamv0alpha1.User{
Spec: iamv0alpha1.UserSpec{Login: "testuser", Role: "Viewer"},
},
newUser: &iamv0alpha1.User{
Spec: iamv0alpha1.UserSpec{Login: "", Email: "test@example", Role: "Viewer"},
},
requester: &identity.StaticRequester{
Type: types.TypeUser,
IsGrafanaAdmin: true,
},
searchClient: &FakeUserLegacySearchClient{},
expectError: false,
},
{
name: "service user verifies email",
oldUser: &iamv0alpha1.User{
Spec: iamv0alpha1.UserSpec{Login: "testuser", EmailVerified: false, Role: "Viewer"},
},
newUser: &iamv0alpha1.User{
Spec: iamv0alpha1.UserSpec{Login: "testuser", EmailVerified: true, Role: "Viewer"},
},
requester: &identity.StaticRequester{
Type: types.TypeAccessPolicy,
},
expectError: false,
},
{
name: "non-service user verifies email",
oldUser: &iamv0alpha1.User{
Spec: iamv0alpha1.UserSpec{Login: "testuser", EmailVerified: false, Role: "Viewer"},
},
newUser: &iamv0alpha1.User{
Spec: iamv0alpha1.UserSpec{Login: "testuser", EmailVerified: true, Role: "Viewer"},
},
requester: &identity.StaticRequester{
Type: types.TypeUser,
},
expectError: true,
errorContains: "only service users can verify email",
},
{
name: "grafana admin disables user",
oldUser: &iamv0alpha1.User{
Spec: iamv0alpha1.UserSpec{Login: "testuser", Disabled: false, Role: "Viewer"},
},
newUser: &iamv0alpha1.User{
Spec: iamv0alpha1.UserSpec{Login: "testuser", Disabled: true, Role: "Viewer"},
},
requester: &identity.StaticRequester{
Type: types.TypeUser,
IsGrafanaAdmin: true,
},
expectError: false,
},
{
name: "non-admin disables user",
oldUser: &iamv0alpha1.User{
Spec: iamv0alpha1.UserSpec{Login: "testuser", Disabled: false, Role: "Viewer"},
},
newUser: &iamv0alpha1.User{
Spec: iamv0alpha1.UserSpec{Login: "testuser", Disabled: true, Role: "Viewer"},
},
requester: &identity.StaticRequester{
Type: types.TypeUser,
IsGrafanaAdmin: false,
},
expectError: true,
errorContains: "only grafana admins can disable or enable a user",
},
{
name: "grafana admin grants admin",
oldUser: &iamv0alpha1.User{
Spec: iamv0alpha1.UserSpec{Login: "testuser", GrafanaAdmin: false, Role: "Viewer"},
},
newUser: &iamv0alpha1.User{
Spec: iamv0alpha1.UserSpec{Login: "testuser", GrafanaAdmin: true, Role: "Viewer"},
},
requester: &identity.StaticRequester{
Type: types.TypeUser,
IsGrafanaAdmin: true,
},
expectError: false,
},
{
name: "non-admin grants admin",
oldUser: &iamv0alpha1.User{
Spec: iamv0alpha1.UserSpec{Login: "testuser", GrafanaAdmin: false, Role: "Viewer"},
},
newUser: &iamv0alpha1.User{
Spec: iamv0alpha1.UserSpec{Login: "testuser", GrafanaAdmin: true, Role: "Viewer"},
},
requester: &identity.StaticRequester{
Type: types.TypeUser,
IsGrafanaAdmin: false,
},
expectError: true,
errorContains: "only grafana admins can change grafana admin status",
},
{
name: "update to empty role",
oldUser: &iamv0alpha1.User{
Spec: iamv0alpha1.UserSpec{Login: "testuser", Role: "Viewer"},
},
newUser: &iamv0alpha1.User{
Spec: iamv0alpha1.UserSpec{Login: "testuser", Role: ""},
},
requester: &identity.StaticRequester{
Type: types.TypeUser,
IsGrafanaAdmin: true,
},
expectError: true,
errorContains: "role is required",
},
{
name: "update to invalid role",
oldUser: &iamv0alpha1.User{
Spec: iamv0alpha1.UserSpec{Login: "testuser", Role: "Viewer"},
},
newUser: &iamv0alpha1.User{
Spec: iamv0alpha1.UserSpec{Login: "testuser", Role: "InvalidRole"},
},
requester: &identity.StaticRequester{
Type: types.TypeUser,
IsGrafanaAdmin: true,
},
expectError: true,
errorContains: "invalid role 'InvalidRole'",
},
{
name: "update to valid role",
oldUser: &iamv0alpha1.User{
Spec: iamv0alpha1.UserSpec{Login: "testuser", Role: "Editor"},
},
newUser: &iamv0alpha1.User{
Spec: iamv0alpha1.UserSpec{Login: "testuser", Role: "Viewer"},
},
requester: &identity.StaticRequester{
Type: types.TypeUser,
IsGrafanaAdmin: true,
},
expectError: false,
},
{
name: "update with existing email",
oldUser: &iamv0alpha1.User{
ObjectMeta: metav1.ObjectMeta{
Name: "userx",
},
Spec: iamv0alpha1.UserSpec{Email: "one@example", Role: "Viewer"},
},
newUser: &iamv0alpha1.User{
ObjectMeta: metav1.ObjectMeta{
Name: "userx",
},
Spec: iamv0alpha1.UserSpec{Email: "two@example", Role: "Viewer"},
},
requester: &identity.StaticRequester{
Type: types.TypeUser,
IsGrafanaAdmin: true,
},
searchClient: &FakeUserLegacySearchClient{
Users: []*org.OrgUserDTO{
{Email: "two@example"},
},
},
expectError: true,
errorContains: "email 'two@example' is already taken",
},
{
name: "update with existing login",
oldUser: &iamv0alpha1.User{
ObjectMeta: metav1.ObjectMeta{
Name: "userx",
},
Spec: iamv0alpha1.UserSpec{Login: "one", Role: "Viewer"},
},
newUser: &iamv0alpha1.User{
ObjectMeta: metav1.ObjectMeta{
Name: "userx",
},
Spec: iamv0alpha1.UserSpec{Login: "two", Role: "Viewer"},
},
requester: &identity.StaticRequester{
Type: types.TypeUser,
IsGrafanaAdmin: true,
},
searchClient: &FakeUserLegacySearchClient{
Users: []*org.OrgUserDTO{
{Name: "other", UID: "uid456", Login: "two"},
},
},
expectError: true,
errorContains: "login 'two' is already taken",
},
{
name: "update with no change to login or email",
oldUser: &iamv0alpha1.User{
Spec: iamv0alpha1.UserSpec{Login: "testuser", Email: "test@example", Role: "Viewer"},
},
newUser: &iamv0alpha1.User{
Spec: iamv0alpha1.UserSpec{Login: "testuser", Email: "test@example", Role: "Editor"},
},
requester: &identity.StaticRequester{
Type: types.TypeUser,
IsGrafanaAdmin: true,
},
searchClient: &FakeUserLegacySearchClient{
Users: []*org.OrgUserDTO{
{Login: "testuser", Email: "test@example"},
},
},
expectError: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctx := identity.WithRequester(
context.Background(),
tt.requester,
)
err := ValidateOnUpdate(ctx, tt.searchClient, tt.oldUser, tt.newUser)
if tt.expectError {
require.Error(t, err)
if tt.errorContains != "" {
require.Contains(t, err.Error(), tt.errorContains)
}
} else {
require.NoError(t, err)
}
})
}
}