From f1497123b926c6cc84bf87e2057be63ac8637359 Mon Sep 17 00:00:00 2001 From: Pritesh Lahoti Date: Wed, 30 Oct 2024 12:37:15 +0530 Subject: [PATCH] [CC-30333] auth: use JWT for authentication This PR allows using the Terraform Provider via JWT authentication, in addition to API Keys. The JWT auth mechanism requires a COCKROACH_VANITY_NAME env var capturing the vanity name of the org with the corresponding JWT Issuer. In case the JWT is issued against multiple identities, it also requires a COCKROACH_USERNAME env var capturing the user / service account to impersonate. Eventually, we will add a CI stage for running acceptance tests via this auth mechanism. --- CHANGELOG.md | 2 + docs/index.md | 8 +++- examples/provider/provider.tf | 3 +- internal/provider/models.go | 13 +++--- internal/provider/provider.go | 63 ++++++++++++++++++++++++------ internal/provider/provider_test.go | 19 +++++---- 6 files changed, 81 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 15457fce..2a4b5461 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Added support for authentication via JWT. + - Setting and fetching of `cidr_range` is now available for GCP Advanced tier clusters. diff --git a/docs/index.md b/docs/index.md index 77060d70..38f996f7 100644 --- a/docs/index.md +++ b/docs/index.md @@ -20,8 +20,9 @@ provider "cockroach" { # Instructions for getting an API Key # https://www.cockroachlabs.com/docs/cockroachcloud/console-access-management.html#api-access # - # The Terraform provider requires an environment variable COCKROACH_API_KEY + # The Terraform provider requires either the COCKROACH_API_KEY or COCKROACH_API_JWT environment variable for performing authentication. # export COCKROACH_API_KEY="the API Key value here" + # export COCKROACH_API_JWT="the JWT value here" } ``` @@ -30,4 +31,7 @@ provider "cockroach" { ### Optional -- `apikey` (String, Sensitive) apikey to access cockroach cloud +- `apijwt` (String, Sensitive) The JWT from a JWT Issuer configured for the CockroachDB Cloud Organization. +In this case, the vanity name of the organization is required and can be provided using the `COCKROACH_VANITY_NAME` environment variable. If the JWT is mapped to multiple identities, the identity to impersonate should be provided using the `COCKROACH_USERNAME` environment variable, and should contain either a user email address or a service account ID. +- `apikey` (String, Sensitive) The API key to access CockroachDB Cloud. +If this field is provided, it is used and `apijwt` is ignored. diff --git a/examples/provider/provider.tf b/examples/provider/provider.tf index cc016ab3..3e802407 100644 --- a/examples/provider/provider.tf +++ b/examples/provider/provider.tf @@ -5,6 +5,7 @@ provider "cockroach" { # Instructions for getting an API Key # https://www.cockroachlabs.com/docs/cockroachcloud/console-access-management.html#api-access # - # The Terraform provider requires an environment variable COCKROACH_API_KEY + # The Terraform provider requires either the COCKROACH_API_KEY or COCKROACH_API_JWT environment variable for performing authentication. # export COCKROACH_API_KEY="the API Key value here" + # export COCKROACH_API_JWT="the JWT value here" } diff --git a/internal/provider/models.go b/internal/provider/models.go index 953d244d..0b8807fb 100644 --- a/internal/provider/models.go +++ b/internal/provider/models.go @@ -23,9 +23,12 @@ import ( ) const ( - CockroachAPIKey string = "COCKROACH_API_KEY" - APIServerURLKey string = "COCKROACH_SERVER" - UserAgent string = "terraform-provider-cockroach" + CockroachAPIKey string = "COCKROACH_API_KEY" + CockroachAPIJWT string = "COCKROACH_API_JWT" + APIServerURLKey string = "COCKROACH_SERVER" + UserAgent string = "terraform-provider-cockroach" + CockroachVanityName string = "COCKROACH_VANITY_NAME" + CockroachUsername string = "COCKROACH_USERNAME" ) type Region struct { @@ -63,8 +66,8 @@ type ClusterBackupConfig struct { } type UsageLimits struct { - RequestUnitLimit types.Int64 `tfsdk:"request_unit_limit"` - StorageMibLimit types.Int64 `tfsdk:"storage_mib_limit"` + RequestUnitLimit types.Int64 `tfsdk:"request_unit_limit"` + StorageMibLimit types.Int64 `tfsdk:"storage_mib_limit"` ProvisionedVirtualCpus types.Int64 `tfsdk:"provisioned_virtual_cpus"` } diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 2184fb65..1b8fd704 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -54,6 +54,7 @@ type provider struct { // providerData can be used to store data from the Terraform configuration. type providerData struct { ApiKey types.String `tfsdk:"apikey"` + ApiJWT types.String `tfsdk:"apijwt"` } func (p *provider) Configure( @@ -74,20 +75,23 @@ func (p *provider) Configure( apiKey = config.ApiKey.ValueString() } - if apiKey == "" { + var apiJWT string + if !IsKnown(config.ApiJWT) { + apiJWT = os.Getenv(CockroachAPIJWT) + } else { + apiJWT = config.ApiJWT.ValueString() + } + + if apiKey == "" && apiJWT == "" { // Error vs warning - empty value must stop execution resp.Diagnostics.AddError( - "Unable to find apikey", - "apikey cannot be an empty string", + "Unable to find authentication token", + "at least one of apikey or apijwt must be provided", ) return } - cfg := client.NewConfiguration(apiKey) - if server := os.Getenv(APIServerURLKey); server != "" { - cfg.ServerURL = server - } - cfg.UserAgent = UserAgent + cfg := getClientConfiguration(apiKey, apiJWT) logLevel := os.Getenv("TF_LOG") if logLevel == "DEBUG" || logLevel == "TRACE" { @@ -167,9 +171,22 @@ func (p *provider) Schema( resp.Schema = schema.Schema{ Attributes: map[string]schema.Attribute{ "apikey": schema.StringAttribute{ - MarkdownDescription: "apikey to access cockroach cloud", - Optional: true, - Sensitive: true, + MarkdownDescription: "The API key to access CockroachDB Cloud.\n" + + "If this field is provided, it is used and `apijwt` is ignored.", + Optional: true, + Sensitive: true, + }, + "apijwt": schema.StringAttribute{ + MarkdownDescription: "The JWT from a JWT Issuer configured for the " + + "CockroachDB Cloud Organization.\n" + + "In this case, the vanity name of the organization is required " + + "and can be provided using the `COCKROACH_VANITY_NAME` environment variable. " + + "If the JWT is mapped to multiple identities, the identity to " + + "impersonate should be provided using the `COCKROACH_USERNAME` environment " + + "variable, and should contain either a user email address or a " + + "service account ID.", + Optional: true, + Sensitive: true, }, }, } @@ -182,3 +199,27 @@ func New(version string) func() tf_provider.Provider { } } } + +func getClientConfiguration(apiKey, apiJWT string) *client.Configuration { + // If the API key is provided, use it, else use the JWT for auth. + apiToken := apiKey + if apiToken == "" { + apiToken = apiJWT + } + + var cfgOpts []client.ConfigurationOption + if vanityName := os.Getenv(CockroachVanityName); vanityName != "" { + cfgOpts = append(cfgOpts, client.WithVanityName(vanityName)) + } + if username := os.Getenv(CockroachUsername); username != "" { + cfgOpts = append(cfgOpts, client.WithUsername(username)) + } + + cfg := client.NewConfiguration(apiToken, cfgOpts...) + if server := os.Getenv(APIServerURLKey); server != "" { + cfg.ServerURL = server + } + cfg.UserAgent = UserAgent + + return cfg +} diff --git a/internal/provider/provider_test.go b/internal/provider/provider_test.go index 3b4f484f..17a4310a 100644 --- a/internal/provider/provider_test.go +++ b/internal/provider/provider_test.go @@ -32,12 +32,11 @@ var testAccProvider tf_provider.Provider var cl *client.Client func init() { - apikey := os.Getenv(CockroachAPIKey) - cfg := client.NewConfiguration(apikey) - if server := os.Getenv(APIServerURLKey); server != "" { - cfg.ServerURL = server - } - cfg.UserAgent = UserAgent + apiKey := os.Getenv(CockroachAPIKey) + apiJWT := os.Getenv(CockroachAPIJWT) + + cfg := getClientConfiguration(apiKey, apiJWT) + cl = client.NewClient(cfg) testAccProvider = New("test")() } @@ -51,8 +50,12 @@ var testAccProtoV6ProviderFactories = map[string]func() (tfprotov6.ProviderServe } func testAccPreCheck(t *testing.T) { - if os.Getenv(CockroachAPIKey) == "" { - t.Fatalf("%s must be set for acceptance testing", CockroachAPIKey) + if os.Getenv(CockroachAPIKey) == "" && os.Getenv(CockroachAPIJWT) == "" { + t.Fatalf( + "%s or %s must be set for acceptance testing", + CockroachAPIKey, + CockroachAPIJWT, + ) } }