Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ease of use: Add validation that org_id isn't used with API Keys/SA Tokens #1296

Merged
merged 1 commit into from
Jan 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions internal/provider/configure_clients.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
118 changes: 62 additions & 56 deletions internal/provider/legacy_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down Expand Up @@ -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{
Expand Down
30 changes: 23 additions & 7 deletions internal/provider/legacy_provider_validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,37 +14,53 @@ 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)
}
return nil
}

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
Expand All @@ -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)
Expand All @@ -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)
Expand Down
67 changes: 67 additions & 0 deletions internal/resources/grafana/resource_team_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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 {
Expand Down