package identity import ( "context" "fmt" "testing" "github.com/grafana/grafana/pkg/apiserver/rest" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/user" "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" "github.com/stretchr/testify/require" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) func TestIntegrationUsers(t *testing.T) { testutil.SkipIntegrationTestInShortMode(t) // TODO: Figure out why rest.Mode4 is failing modes := []rest.DualWriterMode{rest.Mode0, rest.Mode1, rest.Mode2, rest.Mode3} for _, mode := range modes { t.Run(fmt.Sprintf("DualWriterMode %d", mode), func(t *testing.T) { helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ AppModeProduction: false, DisableAnonymous: true, APIServerStorageType: "unified", UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ "users.iam.grafana.app": { DualWriterMode: mode, }, }, EnableFeatureToggles: []string{ featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs, featuremgmt.FlagKubernetesAuthnMutation, }, UnifiedStorageEnableSearch: true, }) t.Cleanup(func() { helper.Shutdown() }) doUserCRUDTestsUsingTheNewAPIs(t, helper) if mode < 3 { doUserCRUDTestsUsingTheLegacyAPIs(t, helper) } }) } } func doUserCRUDTestsUsingTheNewAPIs(t *testing.T, helper *apis.K8sTestHelper) { t.Run("should create user and delete it using the new APIs as a GrafanaAdmin", func(t *testing.T) { ctx := context.Background() userClient := helper.GetResourceClient(apis.ResourceClientArgs{ User: helper.Org1.Admin, Namespace: helper.Namespacer(helper.Org1.Admin.Identity.GetOrgID()), GVR: gvrUsers, }) // Create the user created, err := userClient.Resource.Create(ctx, helper.LoadYAMLOrJSONFile("testdata/user-test-create-v0.yaml"), metav1.CreateOptions{}) require.NoError(t, err) require.NotNil(t, created) // Verify creation response createdSpec := created.Object["spec"].(map[string]interface{}) require.Equal(t, "testuser1@example123", createdSpec["email"]) require.Equal(t, "testuser1", createdSpec["login"]) require.Equal(t, "Test User 1", createdSpec["title"]) require.Equal(t, false, createdSpec["provisioned"]) // Get the UID from created user for fetching createdUID := created.GetName() require.NotEmpty(t, createdUID) _, err = userClient.Resource.List(ctx, metav1.ListOptions{}) require.NoError(t, err) fetched, err := userClient.Resource.Get(ctx, createdUID, metav1.GetOptions{}) require.NoError(t, err) require.NotNil(t, fetched) // Verify fetched user matches created user fetchedSpec := fetched.Object["spec"].(map[string]interface{}) require.Equal(t, "testuser1@example123", fetchedSpec["email"]) require.Equal(t, "testuser1", fetchedSpec["login"]) require.Equal(t, "Test User 1", fetchedSpec["title"]) require.Equal(t, false, fetchedSpec["provisioned"]) // Verify metadata require.Equal(t, createdUID, fetched.GetName()) require.Equal(t, "default", fetched.GetNamespace()) err = userClient.Resource.Delete(ctx, createdUID, metav1.DeleteOptions{}) require.NoError(t, err) // Verify deletion _, err = userClient.Resource.Get(ctx, createdUID, metav1.GetOptions{}) require.Error(t, err) require.Contains(t, err.Error(), "not found") }) t.Run("should update user using the new APIs as a GrafanaAdmin", func(t *testing.T) { ctx := context.Background() userClient := helper.GetResourceClient(apis.ResourceClientArgs{ User: helper.Org1.Admin, Namespace: helper.Namespacer(helper.Org1.Admin.Identity.GetOrgID()), GVR: gvrUsers, }) // Create the user created, err := userClient.Resource.Create(ctx, helper.LoadYAMLOrJSONFile("testdata/user-test-create-v1.yaml"), metav1.CreateOptions{}) require.NoError(t, err) require.NotNil(t, created) // Get the user to update createdUID := created.GetName() userToUpdate, err := userClient.Resource.Get(ctx, createdUID, metav1.GetOptions{}) require.NoError(t, err) // Modify the user spec spec := userToUpdate.Object["spec"].(map[string]interface{}) spec["title"] = "Updated Test User" spec["email"] = "updated.test.user@example" userToUpdate.Object["spec"] = spec // Update the user updated, err := userClient.Resource.Update(ctx, userToUpdate, metav1.UpdateOptions{}) require.NoError(t, err) require.NotNil(t, updated) // Verify the update response updatedSpec := updated.Object["spec"].(map[string]interface{}) require.Equal(t, "Updated Test User", updatedSpec["title"]) require.Equal(t, "updated.test.user@example", updatedSpec["email"]) // Fetch again to confirm fetched, err := userClient.Resource.Get(ctx, createdUID, metav1.GetOptions{}) require.NoError(t, err) fetchedSpec := fetched.Object["spec"].(map[string]interface{}) require.Equal(t, "Updated Test User", fetchedSpec["title"]) require.Equal(t, "updated.test.user@example", fetchedSpec["email"]) // Cleanup err = userClient.Resource.Delete(ctx, fetched.GetName(), metav1.DeleteOptions{}) require.NoError(t, err) }) t.Run("should not be able to create user when using a user with insufficient permissions", func(t *testing.T) { for _, user := range []apis.User{ helper.OrgB.Admin, // Not a Grafana Admin helper.Org1.Editor, helper.Org1.Viewer, } { t.Run(fmt.Sprintf("with basic role_%s", user.Identity.GetOrgRole()), func(t *testing.T) { ctx := context.Background() userClient := helper.GetResourceClient(apis.ResourceClientArgs{ User: user, Namespace: helper.Namespacer(helper.Org1.Admin.Identity.GetOrgID()), GVR: gvrUsers, }) // Create the user _, err := userClient.Resource.Create(ctx, helper.LoadYAMLOrJSONFile("testdata/user-test-create-v0.yaml"), 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 a user with a duplicate email", func(t *testing.T) { ctx := context.Background() userClient := helper.GetResourceClient(apis.ResourceClientArgs{ User: helper.Org1.Admin, Namespace: helper.Namespacer(helper.Org1.Admin.Identity.GetOrgID()), GVR: gvrUsers, }) // Create the first user created, err := userClient.Resource.Create(ctx, helper.LoadYAMLOrJSONFile("testdata/user-test-create-duplicate-email-v0.yaml"), metav1.CreateOptions{}) require.NoError(t, err) require.NotNil(t, created) // Attempt to create another user with the same email _, err = userClient.Resource.Create(ctx, helper.LoadYAMLOrJSONFile("testdata/user-test-create-duplicate-email-other.yaml"), metav1.CreateOptions{}) require.Error(t, err) var statusErr *errors.StatusError require.ErrorAs(t, err, &statusErr) require.Equal(t, int32(409), statusErr.ErrStatus.Code) require.Contains(t, statusErr.ErrStatus.Message, "email 'testuser-email-1@example' is already taken") // Cleanup err = userClient.Resource.Delete(ctx, created.GetName(), metav1.DeleteOptions{}) require.NoError(t, err) }) t.Run("should not be able to create a user with a duplicate login", func(t *testing.T) { ctx := context.Background() userClient := helper.GetResourceClient(apis.ResourceClientArgs{ User: helper.Org1.Admin, Namespace: helper.Namespacer(helper.Org1.Admin.Identity.GetOrgID()), GVR: gvrUsers, }) // Create the first user created, err := userClient.Resource.Create(ctx, helper.LoadYAMLOrJSONFile("testdata/user-test-create-duplicate-login-v0.yaml"), metav1.CreateOptions{}) require.NoError(t, err) require.NotNil(t, created) // Attempt to create a second user with the same login _, err = userClient.Resource.Create(ctx, helper.LoadYAMLOrJSONFile("testdata/user-test-create-duplicate-login-other.yaml"), metav1.CreateOptions{}) require.Error(t, err) var statusErr *errors.StatusError require.ErrorAs(t, err, &statusErr) require.Equal(t, int32(409), statusErr.ErrStatus.Code) require.Contains(t, statusErr.ErrStatus.Message, "login 'testuser-login-1' is already taken") // Cleanup err = userClient.Resource.Delete(ctx, created.GetName(), metav1.DeleteOptions{}) require.NoError(t, err) }) t.Run("should not be able to update a user with an existing email", func(t *testing.T) { ctx := context.Background() userClient := helper.GetResourceClient(apis.ResourceClientArgs{ User: helper.Org1.Admin, Namespace: helper.Namespacer(helper.Org1.Admin.Identity.GetOrgID()), GVR: gvrUsers, }) // Create the first user user1, err := userClient.Resource.Create(ctx, helper.LoadYAMLOrJSONFile("testdata/user-test-create-v0.yaml"), metav1.CreateOptions{}) require.NoError(t, err) require.NotNil(t, user1) // Create the second user user2, err := userClient.Resource.Create(ctx, helper.LoadYAMLOrJSONFile("testdata/user-test-create-v1.yaml"), metav1.CreateOptions{}) require.NoError(t, err) require.NotNil(t, user2) // Get the user to update userToUpdate, err := userClient.Resource.Get(ctx, user2.GetName(), metav1.GetOptions{}) require.NoError(t, err) // Modify the user spec to have the same email as user1 spec := userToUpdate.Object["spec"].(map[string]interface{}) user1Spec := user1.Object["spec"].(map[string]interface{}) spec["email"] = user1Spec["email"] userToUpdate.Object["spec"] = spec // Attempt to update the user _, err = userClient.Resource.Update(ctx, userToUpdate, metav1.UpdateOptions{}) require.Error(t, err) var statusErr *errors.StatusError require.ErrorAs(t, err, &statusErr) require.Equal(t, int32(409), statusErr.ErrStatus.Code) require.Contains(t, statusErr.ErrStatus.Message, "email 'testuser1@example123' is already taken") // Cleanup err = userClient.Resource.Delete(ctx, user1.GetName(), metav1.DeleteOptions{}) require.NoError(t, err) err = userClient.Resource.Delete(ctx, user2.GetName(), metav1.DeleteOptions{}) require.NoError(t, err) }) t.Run("should not be able to update a user with an existing login", func(t *testing.T) { ctx := context.Background() userClient := helper.GetResourceClient(apis.ResourceClientArgs{ User: helper.Org1.Admin, Namespace: helper.Namespacer(helper.Org1.Admin.Identity.GetOrgID()), GVR: gvrUsers, }) // Create the first user user1, err := userClient.Resource.Create(ctx, helper.LoadYAMLOrJSONFile("testdata/user-test-create-v0.yaml"), metav1.CreateOptions{}) require.NoError(t, err) require.NotNil(t, user1) // Create the second user user2, err := userClient.Resource.Create(ctx, helper.LoadYAMLOrJSONFile("testdata/user-test-create-v1.yaml"), metav1.CreateOptions{}) require.NoError(t, err) require.NotNil(t, user2) // Get the user to update userToUpdate, err := userClient.Resource.Get(ctx, user2.GetName(), metav1.GetOptions{}) require.NoError(t, err) // Modify the user spec to have the same login as user1 spec := userToUpdate.Object["spec"].(map[string]interface{}) user1Spec := user1.Object["spec"].(map[string]interface{}) spec["login"] = user1Spec["login"] userToUpdate.Object["spec"] = spec // Attempt to update the user _, err = userClient.Resource.Update(ctx, userToUpdate, metav1.UpdateOptions{}) require.Error(t, err) var statusErr *errors.StatusError require.ErrorAs(t, err, &statusErr) require.Equal(t, int32(409), statusErr.ErrStatus.Code) require.Contains(t, statusErr.ErrStatus.Message, "login 'testuser1' is already taken") // Cleanup err = userClient.Resource.Delete(ctx, user1.GetName(), metav1.DeleteOptions{}) require.NoError(t, err) err = userClient.Resource.Delete(ctx, user2.GetName(), metav1.DeleteOptions{}) require.NoError(t, err) }) } func doUserCRUDTestsUsingTheLegacyAPIs(t *testing.T, helper *apis.K8sTestHelper) { t.Run("should create user using legacy APIs and delete it using the new APIs", func(t *testing.T) { ctx := context.Background() userClient := helper.GetResourceClient(apis.ResourceClientArgs{ User: helper.Org1.Admin, GVR: gvrUsers, }) legacyUserPayload := `{ "name": "Legacy User 3", "email": "legacyuser3@example", "login": "legacyuser3", "password": "password123" }` rsp := apis.DoRequest(helper, apis.RequestParams{ User: helper.Org1.Admin, Method: "POST", Path: "/api/admin/users", Body: []byte(legacyUserPayload), }, &user.User{}) require.NotNil(t, rsp) require.Equal(t, 200, rsp.Response.StatusCode) require.NotEmpty(t, rsp.Result.UID) // Now try to fetch the user via the new API user, err := userClient.Resource.Get(context.Background(), rsp.Result.UID, metav1.GetOptions{}) require.NoError(t, err) require.NotNil(t, user) // Verify fetched user matches created user userSpec := user.Object["spec"].(map[string]interface{}) require.Equal(t, "legacyuser3@example", userSpec["email"]) require.Equal(t, "legacyuser3", userSpec["login"]) require.Equal(t, "Legacy User 3", userSpec["title"]) require.Equal(t, false, userSpec["provisioned"]) // Verify metadata require.Equal(t, rsp.Result.UID, user.GetName()) require.Equal(t, "default", user.GetNamespace()) // Now delete the user using the legacy API deleteRsp := apis.DoRequest(helper, apis.RequestParams{ User: helper.Org1.Admin, Method: "DELETE", Path: fmt.Sprintf("/api/admin/users/%d", rsp.Result.ID), }, &apis.AnyResource{}) require.Equal(t, 200, deleteRsp.Response.StatusCode) // Verify deletion _, err = userClient.Resource.Get(ctx, rsp.Result.UID, metav1.GetOptions{}) require.Error(t, err) require.Contains(t, err.Error(), "not found") }) }