diff --git a/internal/provider/configure_clients.go b/internal/provider/configure_clients.go index 077bba242..a05e5bc62 100644 --- a/internal/provider/configure_clients.go +++ b/internal/provider/configure_clients.go @@ -85,6 +85,10 @@ func createGrafanaOAPIClient(client *common.Client, providerConfig frameworkProv return err } + if orgID > 1 && apiKey != "" { + return fmt.Errorf("org_id is only supported with basic auth. API keys are already org-scoped") + } + cfg := goapi.TransportConfig{ Host: client.GrafanaAPIURLParsed.Host, BasePath: apiPath, diff --git a/internal/provider/legacy_provider.go b/internal/provider/legacy_provider.go index bf510c118..00fa0d99d 100644 --- a/internal/provider/legacy_provider.go +++ b/internal/provider/legacy_provider.go @@ -36,44 +36,47 @@ func init() { func Provider(version string) *schema.Provider { var ( // Resources that require the Grafana client to exist. - grafanaClientResources = addResourcesMetadataValidation(grafanaClientPresent, map[string]*schema.Resource{ - // Grafana - "grafana_annotation": grafana.ResourceAnnotation(), - "grafana_api_key": grafana.ResourceAPIKey(), - "grafana_contact_point": grafana.ResourceContactPoint(), - "grafana_dashboard": grafana.ResourceDashboard(), - "grafana_dashboard_public": grafana.ResourcePublicDashboard(), - "grafana_dashboard_permission": grafana.ResourceDashboardPermission(), - "grafana_data_source": grafana.ResourceDataSource(), - "grafana_data_source_permission": grafana.ResourceDatasourcePermission(), - "grafana_folder": grafana.ResourceFolder(), - "grafana_folder_permission": grafana.ResourceFolderPermission(), - "grafana_library_panel": grafana.ResourceLibraryPanel(), - "grafana_message_template": grafana.ResourceMessageTemplate(), - "grafana_mute_timing": grafana.ResourceMuteTiming(), - "grafana_notification_policy": grafana.ResourceNotificationPolicy(), - "grafana_organization": grafana.ResourceOrganization(), - "grafana_organization_preferences": grafana.ResourceOrganizationPreferences(), - "grafana_playlist": grafana.ResourcePlaylist(), - "grafana_report": grafana.ResourceReport(), - "grafana_role": grafana.ResourceRole(), - "grafana_role_assignment": grafana.ResourceRoleAssignment(), - "grafana_rule_group": grafana.ResourceRuleGroup(), - "grafana_team": grafana.ResourceTeam(), - "grafana_team_external_group": grafana.ResourceTeamExternalGroup(), - "grafana_service_account_token": grafana.ResourceServiceAccountToken(), - "grafana_service_account": grafana.ResourceServiceAccount(), - "grafana_service_account_permission": grafana.ResourceServiceAccountPermission(), - "grafana_user": grafana.ResourceUser(), - - // Machine Learning - "grafana_machine_learning_job": machinelearning.ResourceJob(), - "grafana_machine_learning_holiday": machinelearning.ResourceHoliday(), - "grafana_machine_learning_outlier_detector": machinelearning.ResourceOutlierDetector(), - - // SLO - "grafana_slo": slo.ResourceSlo(), - }) + grafanaClientResources = addCreateReadResourcesMetadataValidation( + readGrafanaClientValidation, + createGrafanaClientValidation, + map[string]*schema.Resource{ + // Grafana + "grafana_annotation": grafana.ResourceAnnotation(), + "grafana_api_key": grafana.ResourceAPIKey(), + "grafana_contact_point": grafana.ResourceContactPoint(), + "grafana_dashboard": grafana.ResourceDashboard(), + "grafana_dashboard_public": grafana.ResourcePublicDashboard(), + "grafana_dashboard_permission": grafana.ResourceDashboardPermission(), + "grafana_data_source": grafana.ResourceDataSource(), + "grafana_data_source_permission": grafana.ResourceDatasourcePermission(), + "grafana_folder": grafana.ResourceFolder(), + "grafana_folder_permission": grafana.ResourceFolderPermission(), + "grafana_library_panel": grafana.ResourceLibraryPanel(), + "grafana_message_template": grafana.ResourceMessageTemplate(), + "grafana_mute_timing": grafana.ResourceMuteTiming(), + "grafana_notification_policy": grafana.ResourceNotificationPolicy(), + "grafana_organization": grafana.ResourceOrganization(), + "grafana_organization_preferences": grafana.ResourceOrganizationPreferences(), + "grafana_playlist": grafana.ResourcePlaylist(), + "grafana_report": grafana.ResourceReport(), + "grafana_role": grafana.ResourceRole(), + "grafana_role_assignment": grafana.ResourceRoleAssignment(), + "grafana_rule_group": grafana.ResourceRuleGroup(), + "grafana_team": grafana.ResourceTeam(), + "grafana_team_external_group": grafana.ResourceTeamExternalGroup(), + "grafana_service_account_token": grafana.ResourceServiceAccountToken(), + "grafana_service_account": grafana.ResourceServiceAccount(), + "grafana_service_account_permission": grafana.ResourceServiceAccountPermission(), + "grafana_user": grafana.ResourceUser(), + + // Machine Learning + "grafana_machine_learning_job": machinelearning.ResourceJob(), + "grafana_machine_learning_holiday": machinelearning.ResourceHoliday(), + "grafana_machine_learning_outlier_detector": machinelearning.ResourceOutlierDetector(), + + // SLO + "grafana_slo": slo.ResourceSlo(), + }) // Resources that require the Synthetic Monitoring client to exist. smClientResources = addResourcesMetadataValidation(smClientPresent, map[string]*schema.Resource{ @@ -106,24 +109,27 @@ func Provider(version string) *schema.Provider { }) // Datasources that require the Grafana client to exist. - grafanaClientDatasources = addResourcesMetadataValidation(grafanaClientPresent, map[string]*schema.Resource{ - "grafana_dashboard": grafana.DatasourceDashboard(), - "grafana_dashboards": grafana.DatasourceDashboards(), - "grafana_data_source": grafana.DatasourceDatasource(), - "grafana_folder": grafana.DatasourceFolder(), - "grafana_folders": grafana.DatasourceFolders(), - "grafana_library_panel": grafana.DatasourceLibraryPanel(), - "grafana_user": grafana.DatasourceUser(), - "grafana_users": grafana.DatasourceUsers(), - "grafana_role": grafana.DatasourceRole(), - "grafana_service_account": grafana.DatasourceServiceAccount(), - "grafana_team": grafana.DatasourceTeam(), - "grafana_organization": grafana.DatasourceOrganization(), - "grafana_organization_preferences": grafana.DatasourceOrganizationPreferences(), - - // SLO - "grafana_slos": slo.DatasourceSlo(), - }) + grafanaClientDatasources = addCreateReadResourcesMetadataValidation( + readGrafanaClientValidation, + createGrafanaClientValidation, + map[string]*schema.Resource{ + "grafana_dashboard": grafana.DatasourceDashboard(), + "grafana_dashboards": grafana.DatasourceDashboards(), + "grafana_data_source": grafana.DatasourceDatasource(), + "grafana_folder": grafana.DatasourceFolder(), + "grafana_folders": grafana.DatasourceFolders(), + "grafana_library_panel": grafana.DatasourceLibraryPanel(), + "grafana_user": grafana.DatasourceUser(), + "grafana_users": grafana.DatasourceUsers(), + "grafana_role": grafana.DatasourceRole(), + "grafana_service_account": grafana.DatasourceServiceAccount(), + "grafana_team": grafana.DatasourceTeam(), + "grafana_organization": grafana.DatasourceOrganization(), + "grafana_organization_preferences": grafana.DatasourceOrganizationPreferences(), + + // SLO + "grafana_slos": slo.DatasourceSlo(), + }) // Datasources that require the Synthetic Monitoring client to exist. smClientDatasources = addResourcesMetadataValidation(smClientPresent, map[string]*schema.Resource{ diff --git a/internal/provider/legacy_provider_validation.go b/internal/provider/legacy_provider_validation.go index 2f33e47ba..59c181277 100644 --- a/internal/provider/legacy_provider_validation.go +++ b/internal/provider/legacy_provider_validation.go @@ -14,30 +14,42 @@ import ( // These validations are added to the Create and Read functions of all resources, // because they are entrypoints (code that will be run in all cases). -type metadataValidation func(resourceName string, m interface{}) error +type metadataValidation func(resourceName string, d *schema.ResourceData, m interface{}) error -func grafanaClientPresent(resourceName string, m interface{}) error { +func readGrafanaClientValidation(resourceName string, d *schema.ResourceData, m interface{}) error { if m.(*common.Client).GrafanaOAPI == nil { return fmt.Errorf("the Grafana client is required for `%s`. Set the auth and url provider attributes", resourceName) } return nil } -func smClientPresent(resourceName string, m interface{}) error { +func createGrafanaClientValidation(resourceName string, d *schema.ResourceData, m interface{}) error { + if err := readGrafanaClientValidation(resourceName, d, m); err != nil { + return err + } + orgID, ok := d.GetOk("org_id") + orgIDStr, orgIDOk := orgID.(string) + if ok && orgIDOk && orgIDStr != "" && orgIDStr != "0" && m.(*common.Client).GrafanaAPIConfig.APIKey != "" { + return fmt.Errorf("org_id is only supported with basic auth. API keys are already org-scoped") + } + return nil +} + +func smClientPresent(resourceName string, d *schema.ResourceData, m interface{}) error { if m.(*common.Client).SMAPI == nil { return fmt.Errorf("the Synthetic Monitoring client is required for `%s`. Set the sm_access_token provider attribute", resourceName) } return nil } -func cloudClientPresent(resourceName string, m interface{}) error { +func cloudClientPresent(resourceName string, d *schema.ResourceData, m interface{}) error { if m.(*common.Client).GrafanaCloudAPI == nil { return fmt.Errorf("the Cloud API client is required for `%s`. Set the cloud_api_key provider attribute", resourceName) } return nil } -func onCallClientPresent(resourceName string, m interface{}) error { +func onCallClientPresent(resourceName string, d *schema.ResourceData, m interface{}) error { if m.(*common.Client).OnCallClient == nil { return fmt.Errorf("the Oncall client is required for `%s`. Set the oncall_access_token provider attribute", resourceName) } @@ -45,6 +57,10 @@ func onCallClientPresent(resourceName string, m interface{}) error { } func addResourcesMetadataValidation(validateFunc metadataValidation, resources map[string]*schema.Resource) map[string]*schema.Resource { + return addCreateReadResourcesMetadataValidation(validateFunc, validateFunc, resources) +} + +func addCreateReadResourcesMetadataValidation(readValidateFunc, createValidateFunc metadataValidation, resources map[string]*schema.Resource) map[string]*schema.Resource { for name, r := range resources { name := name //nolint:staticcheck @@ -54,7 +70,7 @@ func addResourcesMetadataValidation(validateFunc metadataValidation, resources m if r.ReadContext != nil { prev := r.ReadContext r.ReadContext = func(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { - if err := validateFunc(name, m); err != nil { + if err := readValidateFunc(name, d, m); err != nil { return diag.FromErr(err) } return prev(ctx, d, m) @@ -67,7 +83,7 @@ func addResourcesMetadataValidation(validateFunc metadataValidation, resources m if r.CreateContext != nil { prev := r.CreateContext r.CreateContext = func(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { - if err := validateFunc(name, m); err != nil { + if err := createValidateFunc(name, d, m); err != nil { return diag.FromErr(err) } return prev(ctx, d, m) diff --git a/internal/resources/grafana/resource_team_test.go b/internal/resources/grafana/resource_team_test.go index d43423638..82858b046 100644 --- a/internal/resources/grafana/resource_team_test.go +++ b/internal/resources/grafana/resource_team_test.go @@ -2,9 +2,13 @@ package grafana_test import ( "fmt" + "os" + "regexp" + "strconv" "strings" "testing" + "github.com/grafana/grafana-openapi-client-go/client/service_accounts" "github.com/grafana/grafana-openapi-client-go/models" "github.com/grafana/terraform-provider-grafana/internal/common" "github.com/grafana/terraform-provider-grafana/internal/resources/grafana" @@ -317,6 +321,69 @@ func TestAccResourceTeam_InOrg(t *testing.T) { }) } +// This tests that API keys/service account tokens cannot be used at the same time as org_id +// because API keys are already org-scoped. +func TestAccTeam_OrgScopedOnAPIKey(t *testing.T) { + testutils.CheckOSSTestsEnabled(t, ">=9.1.0") + + // Create a service account within an org + name := acctest.RandString(10) + globalClient := grafana.OAPIGlobalClient(testutils.Provider.Meta()) + org, err := globalClient.Orgs.CreateOrg(&models.CreateOrgCommand{Name: name}) + if err != nil { + t.Fatal(err) + } + defer func() { + if _, err := globalClient.Orgs.DeleteOrgByID(*org.Payload.OrgID); err != nil { + t.Fatal(err) + } + }() + orgClient := grafana.OAPIGlobalClient(testutils.Provider.Meta()).WithOrgID(*org.Payload.OrgID) + sa, err := orgClient.ServiceAccounts.CreateServiceAccount( + service_accounts.NewCreateServiceAccountParams().WithBody(&models.CreateServiceAccountForm{ + Name: name, + Role: "Admin", + }, + )) + if err != nil { + t.Fatal(err) + } + saToken, err := orgClient.ServiceAccounts.CreateToken( + service_accounts.NewCreateTokenParams().WithBody(&models.AddServiceAccountTokenCommand{ + Name: name, + }, + ).WithServiceAccountID(sa.Payload.ID), + ) + if err != nil { + t.Fatal(err) + } + + prevAuth := os.Getenv("GRAFANA_AUTH") + os.Setenv("GRAFANA_AUTH", saToken.Payload.Key) + defer os.Setenv("GRAFANA_AUTH", prevAuth) + resource.Test(t, resource.TestCase{ + ProviderFactories: testutils.ProviderFactories, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(`resource "grafana_team" "test" { + org_id = %d + name = "test" + }`, *org.Payload.OrgID), + ExpectError: regexp.MustCompile("org_id is only supported with basic auth. API keys are already org-scoped"), + }, + { + Config: `resource "grafana_team" "test" { + name = "test" + }`, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("grafana_team.test", "name", "test"), + resource.TestCheckResourceAttr("grafana_team.test", "org_id", strconv.FormatInt(*org.Payload.OrgID, 10)), + ), + }, + }, + }) +} + func testAccTeamDefinition(name string, teamMembers []string, withPreferences bool, externalGroups []string) string { withPreferencesBlock := "" if withPreferences {