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/runtime/schema" "github.com/grafana/grafana/pkg/apiserver/rest" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/serviceaccounts" "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" ) var gvrServiceAccounts = schema.GroupVersionResource{ Group: "iam.grafana.app", Version: "v0alpha1", Resource: "serviceaccounts", } func TestIntegrationServiceAccounts(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("Service Account 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{ "serviceaccounts.iam.grafana.app": { DualWriterMode: mode, }, }, EnableFeatureToggles: []string{ featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs, featuremgmt.FlagKubernetesAuthnMutation, }, }) doServiceAccountCRUDTestsUsingTheNewAPIs(t, helper) if mode < 3 { doServiceAccountCRUDTestsUsingTheLegacyAPIs(t, helper) } }) } } func doServiceAccountCRUDTestsUsingTheNewAPIs(t *testing.T, helper *apis.K8sTestHelper) { t.Run("should create service account and get it using the new APIs as admin", func(t *testing.T) { ctx := context.Background() saClient := helper.GetResourceClient(apis.ResourceClientArgs{ User: helper.Org1.Admin, Namespace: helper.Namespacer(helper.Org1.Admin.Identity.GetOrgID()), GVR: gvrServiceAccounts, }) created, err := saClient.Resource.Create(ctx, helper.LoadYAMLOrJSONFile("testdata/serviceaccount-test-create-v0.yaml"), metav1.CreateOptions{}) require.NoError(t, err) require.NotNil(t, created) createdSpec := created.Object["spec"].(map[string]interface{}) require.Equal(t, "Test Service Account 1", createdSpec["title"]) require.Equal(t, false, createdSpec["disabled"]) require.Empty(t, createdSpec["plugin"]) createdUID := created.GetName() require.NotEmpty(t, createdUID) _, err = saClient.Resource.List(ctx, metav1.ListOptions{}) require.NoError(t, err) fetched, err := saClient.Resource.Get(ctx, createdUID, metav1.GetOptions{}) require.NoError(t, err) require.NotNil(t, fetched) fetchedSpec := fetched.Object["spec"].(map[string]interface{}) require.Equal(t, "Test Service Account 1", fetchedSpec["title"]) require.Equal(t, false, fetchedSpec["disabled"]) require.Empty(t, fetchedSpec["plugin"]) require.Equal(t, createdUID, fetched.GetName()) require.Equal(t, "default", fetched.GetNamespace()) err = saClient.Resource.Delete(ctx, createdUID, metav1.DeleteOptions{}) require.NoError(t, err) _, err = saClient.Resource.Get(ctx, createdUID, metav1.GetOptions{}) require.Error(t, err) var statusErr *errors.StatusError require.ErrorAs(t, err, &statusErr) require.Equal(t, int32(404), statusErr.ErrStatus.Code) }) t.Run("should not be able to create service account 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() saClient := helper.GetResourceClient(apis.ResourceClientArgs{ User: user, Namespace: helper.Namespacer(helper.Org1.Admin.Identity.GetOrgID()), GVR: gvrServiceAccounts, }) _, err := saClient.Resource.Create(ctx, helper.LoadYAMLOrJSONFile("testdata/serviceaccount-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 service account with invalid role", func(t *testing.T) { ctx := context.Background() saClient := helper.GetResourceClient(apis.ResourceClientArgs{ User: helper.Org1.Admin, Namespace: helper.Namespacer(helper.Org1.Admin.Identity.GetOrgID()), GVR: gvrServiceAccounts, }) saToCreate := helper.LoadYAMLOrJSONFile("testdata/serviceaccount-test-invalid-role-v0.yaml") _, err := saClient.Resource.Create(ctx, saToCreate, 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 role: InvalidRole") }) t.Run("should not be able to create service account without a title", func(t *testing.T) { ctx := context.Background() saClient := helper.GetResourceClient(apis.ResourceClientArgs{ User: helper.Org1.Admin, Namespace: helper.Namespacer(helper.Org1.Admin.Identity.GetOrgID()), GVR: gvrServiceAccounts, }) saToCreate := helper.LoadYAMLOrJSONFile("testdata/serviceaccount-test-no-title-v0.yaml") _, err := saClient.Resource.Create(ctx, saToCreate, 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, "service account must have a title") }) t.Run("should not be able to create external service account as a user", func(t *testing.T) { ctx := context.Background() saClient := helper.GetResourceClient(apis.ResourceClientArgs{ User: helper.Org1.Admin, Namespace: helper.Namespacer(helper.Org1.Admin.Identity.GetOrgID()), GVR: gvrServiceAccounts, }) saToCreate := helper.LoadYAMLOrJSONFile("testdata/serviceaccount-test-external-v0.yaml") _, err := saClient.Resource.Create(ctx, saToCreate, metav1.CreateOptions{}) require.Error(t, err) var statusErr *errors.StatusError require.ErrorAs(t, err, &statusErr) require.Equal(t, int32(403), statusErr.ErrStatus.Code) require.Contains(t, statusErr.ErrStatus.Message, "only service identities can create external service accounts") }) t.Run("should create service account with generateName and get it using the new APIs as a GrafanaAdmin", func(t *testing.T) { ctx := context.Background() saClient := helper.GetResourceClient(apis.ResourceClientArgs{ User: helper.Org1.Admin, Namespace: helper.Namespacer(helper.Org1.Admin.Identity.GetOrgID()), GVR: gvrServiceAccounts, }) created, err := saClient.Resource.Create(ctx, helper.LoadYAMLOrJSONFile("testdata/serviceaccount-test-generate-name-v0.yaml"), metav1.CreateOptions{}) require.NoError(t, err) require.NotNil(t, created) createdSpec := created.Object["spec"].(map[string]interface{}) require.Equal(t, "Test Service Account with GenerateName", createdSpec["title"]) require.Equal(t, false, createdSpec["disabled"]) require.Empty(t, createdSpec["plugin"]) createdUID := created.GetName() require.NotEmpty(t, createdUID) require.Contains(t, createdUID, "sa-") _, err = saClient.Resource.List(ctx, metav1.ListOptions{}) require.NoError(t, err) fetched, err := saClient.Resource.Get(ctx, createdUID, metav1.GetOptions{}) require.NoError(t, err) require.NotNil(t, fetched) fetchedSpec := fetched.Object["spec"].(map[string]interface{}) require.Equal(t, "Test Service Account with GenerateName", fetchedSpec["title"]) require.Equal(t, false, fetchedSpec["disabled"]) require.Empty(t, fetchedSpec["plugin"]) require.Equal(t, createdUID, fetched.GetName()) require.Equal(t, "default", fetched.GetNamespace()) }) } func doServiceAccountCRUDTestsUsingTheLegacyAPIs(t *testing.T, helper *apis.K8sTestHelper) { t.Run("should create service account using legacy APIs and get it using the new APIs", func(t *testing.T) { ctx := context.Background() saClient := helper.GetResourceClient(apis.ResourceClientArgs{ User: helper.Org1.Admin, Namespace: helper.Namespacer(helper.Org1.Admin.Identity.GetOrgID()), GVR: gvrServiceAccounts, }) legacySAPayload := `{ "name": "Test Service Account 2", "role": "Viewer" }` rsp := apis.DoRequest(helper, apis.RequestParams{ User: helper.Org1.Admin, Method: "POST", Path: "/api/serviceaccounts", Body: []byte(legacySAPayload), }, &serviceaccounts.ServiceAccountDTO{}) require.NotNil(t, rsp) require.Equal(t, 201, rsp.Response.StatusCode) require.NotEmpty(t, rsp.Result.UID) sa, err := saClient.Resource.Get(ctx, rsp.Result.UID, metav1.GetOptions{}) require.NoError(t, err) require.NotNil(t, sa) saSpec := sa.Object["spec"].(map[string]interface{}) require.Equal(t, "Test Service Account 2", saSpec["title"]) require.Equal(t, false, saSpec["disabled"]) require.Empty(t, saSpec["plugin"]) require.Equal(t, rsp.Result.UID, sa.GetName()) require.Equal(t, "default", sa.GetNamespace()) }) t.Run("should create service account using legacy APIs and delete it using the new APIs", func(t *testing.T) { ctx := context.Background() saClient := helper.GetResourceClient(apis.ResourceClientArgs{ User: helper.Org1.Admin, Namespace: helper.Namespacer(helper.Org1.Admin.Identity.GetOrgID()), GVR: gvrServiceAccounts, }) legacySAPayload := `{ "name": "Test Service Account to delete", "role": "Editor" }` rsp := apis.DoRequest(helper, apis.RequestParams{ User: helper.Org1.Admin, Method: "POST", Path: "/api/serviceaccounts", Body: []byte(legacySAPayload), }, &serviceaccounts.ServiceAccountDTO{}) require.NotNil(t, rsp) require.Equal(t, 201, rsp.Response.StatusCode) require.NotEmpty(t, rsp.Result.UID) _, err := saClient.Resource.Get(ctx, rsp.Result.UID, metav1.GetOptions{}) require.NoError(t, err) err = saClient.Resource.Delete(ctx, rsp.Result.UID, metav1.DeleteOptions{}) require.NoError(t, err) _, err = saClient.Resource.Get(ctx, rsp.Result.UID, metav1.GetOptions{}) require.Error(t, err) var statusErr *errors.StatusError require.ErrorAs(t, err, &statusErr) require.Equal(t, int32(404), statusErr.ErrStatus.Code) }) }