IAM: Create and delete user from the legacy store (#107694)

* Add Create for User + DualWriter setup

* Add delete User

* Fix delete + access check

* Add tests for delete user

* Add tests for create user

* Fixes

* Use sqlx session to fix database locked issues

* wip authz checks

* legacyAccessClient

* Update legacyAccessClient, add tests for create user

* Close rows before running other queries

* Use ExecWithReturningId

* Verify deletion in the tests

* Add Validate and Mutate

* Other changes

* Address feedback

* Update tests

---------

Co-authored-by: Gabriel Mabille <gabriel.mabille@grafana.com>
This commit is contained in:
Misi
2025-07-17 11:50:40 +02:00
committed by GitHub
co-authored by Gabriel Mabille
parent 3e29ca786d
commit c6a6b9fdd2
45 changed files with 1683 additions and 102 deletions
+165
View File
@@ -2,13 +2,17 @@ package identity
import (
"context"
"fmt"
"testing"
"github.com/stretchr/testify/require"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"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/tests/testsuite"
@@ -173,3 +177,164 @@ func TestIntegrationIdentity(t *testing.T) {
]`, found)
})
}
func TestIntegrationUsers(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
// 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("User 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{
"users.iam.grafana.app": {
DualWriterMode: mode,
},
},
EnableFeatureToggles: []string{
featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs,
},
})
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", func(t *testing.T) {
ctx := context.Background()
userClient := helper.GetResourceClient(apis.ResourceClientArgs{
User: helper.Org1.Admin,
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.com", createdSpec["email"])
require.Equal(t, "testuser1", createdSpec["login"])
require.Equal(t, "Test User 1", createdSpec["name"])
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.com", fetchedSpec["email"])
require.Equal(t, "testuser1", fetchedSpec["login"])
require.Equal(t, "Test User 1", fetchedSpec["name"])
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 not be able to create user when using a user with insufficient permissions", func(t *testing.T) {
for _, user := range []apis.User{
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,
GVR: gvrUsers,
})
// Create the user
_, err := userClient.Resource.Create(ctx, helper.LoadYAMLOrJSONFile("testdata/user-test-create-v0.yaml"), metav1.CreateOptions{})
require.Error(t, err)
require.Contains(t, err.Error(), "unauthorized request")
})
}
})
}
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": "Test User 2",
"email": "testuser2@example.com",
"login": "testuser2",
"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, "testuser2@example.com", userSpec["email"])
require.Equal(t, "testuser2", userSpec["login"])
require.Equal(t, "Test User 2", userSpec["name"])
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")
})
}
+10
View File
@@ -0,0 +1,10 @@
apiVersion: iam.grafana.app/v0alpha1
kind: User
metadata:
namespace: default
name: abcdefghijkl
spec:
email: testuser1@example123.com
login: testuser1
name: Test User 1
provisioned: false