diff --git a/docs/index.md b/docs/index.md index 8f9c668..7803495 100644 --- a/docs/index.md +++ b/docs/index.md @@ -67,13 +67,16 @@ provider "restapi" { ### Nested Schema for `oauth_client_credentials` +NOTE: One of `oauth_client_id_environment_variable` and `oauth_client_secret_environment_variable` or `oauth_client_id` and `oauth_client_secret` MUST be set if this block is configured. If both are set environment variables take priority. + Required: -- `oauth_client_id` (String) client id -- `oauth_client_secret` (String) client secret - `oauth_token_endpoint` (String) oauth token endpoint Optional: - +- `oauth_client_id_environment_variable` (String) client id +- `oauth_client_secret_environment_variable` (String) client secret +- `oauth_client_id` (String) client id +- `oauth_client_secret` (String) client secret - `endpoint_params` (Map of String) Additional key/values to pass to the underlying Oauth client library (as EndpointParams) - `oauth_scopes` (List of String) scopes diff --git a/examples/workingexamples/provider_with_oauth_env.tf b/examples/workingexamples/provider_with_oauth_env.tf new file mode 100644 index 0000000..6ad4561 --- /dev/null +++ b/examples/workingexamples/provider_with_oauth_env.tf @@ -0,0 +1,22 @@ +provider "restapi" { + alias = "restapi_oauth_env" + uri = "https://graph.microsoft.com/beta/" + write_returns_object = true + debug = true + + oauth_client_credentials { + oauth_client_id_environment_variable = "ARM_CLIENT_ID" + oauth_client_secret_environment_variable = "ARM_CLIENT_SECRET" + oauth_token_endpoint = "https://login.microsoft.com/${var.tenantId}/oauth2/v2.0/token" + oauth_scopes = ["https://graph.microsoft.com/.default"] + endpoint_params = {"grant_type"="client_credentials"} + } + + headers = { + "Content-Type" = "application/json" + } + + create_method = "PUT" + update_method = "PATCH" + destroy_method = "DELETE" +} \ No newline at end of file diff --git a/restapi/api_client.go b/restapi/api_client.go index fed7be0..f28af65 100644 --- a/restapi/api_client.go +++ b/restapi/api_client.go @@ -23,38 +23,40 @@ import ( ) type apiClientOpt struct { - uri string - insecure bool - username string - password string - headers map[string]string - timeout int - idAttribute string - createMethod string - readMethod string - readData string - updateMethod string - updateData string - destroyMethod string - destroyData string - copyKeys []string - writeReturnsObject bool - createReturnsObject bool - xssiPrefix string - useCookies bool - rateLimit float64 - oauthClientID string - oauthClientSecret string - oauthScopes []string - oauthTokenURL string - oauthEndpointParams url.Values - certFile string - keyFile string - rootCAFile string - certString string - keyString string - rootCAString string - debug bool + uri string + insecure bool + username string + password string + headers map[string]string + timeout int + idAttribute string + createMethod string + readMethod string + readData string + updateMethod string + updateData string + destroyMethod string + destroyData string + copyKeys []string + writeReturnsObject bool + createReturnsObject bool + xssiPrefix string + useCookies bool + rateLimit float64 + oauthClientIDEnvVar string + oauthClientSecretEnvVar string + oauthClientID string + oauthClientSecret string + oauthScopes []string + oauthTokenURL string + oauthEndpointParams url.Values + certFile string + keyFile string + rootCAFile string + certString string + keyString string + rootCAString string + debug bool } /*APIClient is a HTTP client with additional controlling fields*/ @@ -82,6 +84,15 @@ type APIClient struct { oauthConfig *clientcredentials.Config } +// Helper function for optional environment imports +func GetEnvStringOrDefault(key, def string) string { + if env := os.Getenv(key); env != "" { + log.Printf("Got env for %s", key) + return env + } + return def +} + // NewAPIClient makes a new api client for RESTful calls func NewAPIClient(opt *apiClientOpt) (*APIClient, error) { if opt.debug { @@ -205,10 +216,13 @@ func NewAPIClient(opt *apiClientOpt) (*APIClient, error) { debug: opt.debug, } - if opt.oauthClientID != "" && opt.oauthClientSecret != "" && opt.oauthTokenURL != "" { + resolvedClientID := GetEnvStringOrDefault(opt.oauthClientIDEnvVar, opt.oauthClientID) + resolvedClientSecret := GetEnvStringOrDefault(opt.oauthClientSecretEnvVar, opt.oauthClientSecret) + + if resolvedClientID != "" && resolvedClientSecret != "" && opt.oauthTokenURL != "" { client.oauthConfig = &clientcredentials.Config{ - ClientID: opt.oauthClientID, - ClientSecret: opt.oauthClientSecret, + ClientID: resolvedClientID, + ClientSecret: resolvedClientSecret, TokenURL: opt.oauthTokenURL, Scopes: opt.oauthScopes, EndpointParams: opt.oauthEndpointParams, diff --git a/restapi/provider.go b/restapi/provider.go index 4f079df..01a62ef 100644 --- a/restapi/provider.go +++ b/restapi/provider.go @@ -135,15 +135,25 @@ func Provider() *schema.Provider { Description: "Configuration for oauth client credential flow using the https://pkg.go.dev/golang.org/x/oauth2 implementation", Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ + "oauth_client_id_environment_variable": { + Type: schema.TypeString, + Description: "Name of client id environment variable, when using an environment variable to specify the client id", + Optional: true, + }, + "oauth_client_secret_environment_variable": { + Type: schema.TypeString, + Description: "Name of client secret environment variable, when using an environment variable to specify the client id", + Optional: true, + }, "oauth_client_id": { Type: schema.TypeString, Description: "client id", - Required: true, + Optional: true, }, "oauth_client_secret": { Type: schema.TypeString, Description: "client secret", - Required: true, + Optional: true, }, "oauth_token_endpoint": { Type: schema.TypeString, @@ -214,6 +224,7 @@ func Provider() *schema.Provider { "restapi_object": dataSourceRestAPI(), }, ConfigureFunc: configureProvider, + } } @@ -267,6 +278,24 @@ func configureProvider(d *schema.ResourceData) (interface{}, error) { if v, ok := d.GetOk("oauth_client_credentials"); ok { oauthConfig := v.([]interface{})[0].(map[string]interface{}) + if (oauthConfig["oauth_client_id_environment_variable"] == "" && oauthConfig["oauth_client_secret_environment_variable"] == "" && oauthConfig["oauth_client_id"] == "" && oauthConfig["oauth_client_secret"] == "") { + return nil, fmt.Errorf("If configuring oauth, either `oauth_client_id_environment_variable`, `oauth_client_secret_environment_variable` OR `oauth_client_id` and `oauth_client_secret` must be specified") + } + if (oauthConfig["oauth_client_id_environment_variable"] != "" && oauthConfig["oauth_client_secret_environment_variable"] == "") { + return nil, fmt.Errorf("`oauth_client_id_environment_variable` is configured, but `oauth_client_secret_environment_variable` is missing") + } + if (oauthConfig["oauth_client_id_environment_variable"] == "" && oauthConfig["oauth_client_secret_environment_variable"] != "") { + return nil, fmt.Errorf("`oauth_client_secret_environment_variable` is configured, but `oauth_client_id_environment_variable` is missing") + } + if (oauthConfig["oauth_client_id"] != "" && oauthConfig["oauth_client_secret"] == "") { + return nil, fmt.Errorf("`oauth_client_id` is configured, but `oauth_client_secret` is missing") + } + if (oauthConfig["oauth_client_id"] == "" && oauthConfig["oauth_client_secret"] != "") { + return nil, fmt.Errorf("`oauth_client_secret` is configured, but `oauth_client_id` is missing") + } + + opt.oauthClientIDEnvVar = oauthConfig["oauth_client_id_environment_variable"].(string) + opt.oauthClientSecretEnvVar = oauthConfig["oauth_client_secret_environment_variable"].(string) opt.oauthClientID = oauthConfig["oauth_client_id"].(string) opt.oauthClientSecret = oauthConfig["oauth_client_secret"].(string) opt.oauthTokenURL = oauthConfig["oauth_token_endpoint"].(string) diff --git a/restapi/provider_test.go b/restapi/provider_test.go index c386ea7..8c9d166 100644 --- a/restapi/provider_test.go +++ b/restapi/provider_test.go @@ -68,6 +68,27 @@ func TestResourceProvider_Oauth(t *testing.T) { } } +func TestResourceProvider_Oauth_Env(t *testing.T) { + rp := Provider() + raw := map[string]interface{}{ + "uri": "http://foo.bar/baz", + "oauth_client_credentials": map[string]interface{}{ + "oauth_client_id_environment_variable": "test", + "oauth_client_secret_environment_variable": "secret", + }, + } + + /* + XXX: This is expected to work even though we are not + explicitly declaring the required url parameter since + the test suite is run with the ENV entry set. + */ + err := rp.Configure(context.TODO(), terraform.NewResourceConfigRaw(raw)) + if err != nil { + t.Fatalf("Provider failed with error: %v", err) + } +} + func TestResourceProvider_RequireTestPath(t *testing.T) { debug := false apiServerObjects := make(map[string]map[string]interface{})