From f9fc0b000f24b6aa225c4d9a2a56810c05bbba7d Mon Sep 17 00:00:00 2001 From: Ryan Mottley Date: Sun, 17 Dec 2023 21:57:56 -0600 Subject: [PATCH] feat(provider): allow iamtoken and crn in lieu of servicekey in provider For IBM envs, we will allow provider configs to contain iamtoken and cloud_resource_name to authorize with the API in IBM envs in lieu of servicekey Semver: minor Ref: LOG-18869 --- logdna/provider.go | 30 ++++++++++--- logdna/request.go | 50 ++++++++++++++-------- logdna/request_test.go | 97 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 153 insertions(+), 24 deletions(-) diff --git a/logdna/provider.go b/logdna/provider.go index 3d08946..2799a8b 100644 --- a/logdna/provider.go +++ b/logdna/provider.go @@ -8,9 +8,11 @@ import ( ) type providerConfig struct { - serviceKey string - baseURL string - httpClient *http.Client + serviceKey string + iamtoken string + cloud_resource_name string + baseURL string + httpClient *http.Client } // Provider initializes the schema with a service key and hooks for our resources @@ -18,8 +20,18 @@ func Provider() *schema.Provider { return &schema.Provider{ Schema: map[string]*schema.Schema{ "servicekey": { + Type: schema.TypeString, + Optional: true, + ExactlyOneOf: []string{"iamtoken"}, + }, + "iamtoken": { + Type: schema.TypeString, + Optional: true, + RequiredWith: []string{"cloud_resource_name"}, + }, + "cloud_resource_name": { Type: schema.TypeString, - Required: true, + Optional: true, }, "url": { Type: schema.TypeString, @@ -48,11 +60,15 @@ func Provider() *schema.Provider { func providerConfigure(d *schema.ResourceData) (interface{}, error) { serviceKey := d.Get("servicekey").(string) + iamtoken := d.Get("iamtoken").(string) + cloud_resource_name := d.Get("cloud_resource_name").(string) url := d.Get("url").(string) return &providerConfig{ - serviceKey: serviceKey, - baseURL: url, - httpClient: &http.Client{Timeout: 15 * time.Second}, + serviceKey: serviceKey, + iamtoken: iamtoken, + cloud_resource_name: cloud_resource_name, + baseURL: url, + httpClient: &http.Client{Timeout: 15 * time.Second}, }, nil } diff --git a/logdna/request.go b/logdna/request.go index 54288ee..e7048e9 100644 --- a/logdna/request.go +++ b/logdna/request.go @@ -17,27 +17,31 @@ type httpClientInterface interface { // Configuration for the HTTP client used to make requests to remote resources type requestConfig struct { - serviceKey string - httpClient httpClientInterface - apiURL string - method string - body interface{} - httpRequest httpRequest - bodyReader bodyReader - jsonMarshal jsonMarshal + serviceKey string + iamtoken string + cloud_resource_name string + httpClient httpClientInterface + apiURL string + method string + body interface{} + httpRequest httpRequest + bodyReader bodyReader + jsonMarshal jsonMarshal } // newRequestConfig abstracts the struct creation to allow for mocking func newRequestConfig(pc *providerConfig, method string, uri string, body interface{}, mutators ...func(*requestConfig)) *requestConfig { rc := &requestConfig{ - serviceKey: pc.serviceKey, - httpClient: pc.httpClient, - apiURL: fmt.Sprintf("%s%s", pc.baseURL, uri), // uri should have a preceding slash (/) - method: method, - body: body, - httpRequest: http.NewRequest, - bodyReader: io.ReadAll, - jsonMarshal: json.Marshal, + serviceKey: pc.serviceKey, + iamtoken: pc.iamtoken, + cloud_resource_name: pc.cloud_resource_name, + httpClient: pc.httpClient, + apiURL: fmt.Sprintf("%s%s", pc.baseURL, uri), // uri should have a preceding slash (/) + method: method, + body: body, + httpRequest: http.NewRequest, + bodyReader: io.ReadAll, + jsonMarshal: json.Marshal, } // Used during testing only; Allow mutations passed in by tests @@ -64,7 +68,19 @@ func (c *requestConfig) MakeRequest() ([]byte, error) { if payloadBuf.Len() > 0 { req.Header.Set("Content-Type", "application/json") } - req.Header.Set("servicekey", c.serviceKey) + + // Set the correct authorization headers depending on what has been passed in + // the provider config + if c.serviceKey != "" { + req.Header.Set("servicekey", c.serviceKey) + } else if c.iamtoken != "" && c.cloud_resource_name != "" { + req.Header.Set("Authorization", "Bearer "+c.iamtoken) + req.Header.Set("cloud-resource-name", c.cloud_resource_name) + } else { + err := fmt.Errorf("expected either servicekey or iamtoken to be set") + return nil, err + } + res, err := c.httpClient.Do(req) if err != nil { return nil, fmt.Errorf("error during HTTP request: %s", err) diff --git a/logdna/request_test.go b/logdna/request_test.go index 142965c..6e99ad6 100644 --- a/logdna/request_test.go +++ b/logdna/request_test.go @@ -70,6 +70,44 @@ func TestRequest_MakeRequest(t *testing.T) { assert.Nil(err, "No errors") }) + t.Run("Server receives proper method, URL, and headers for request with a body, iamtoken", func(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal("GET", r.Method, "method is correct") + assert.Equal(fmt.Sprintf("/someapi/%s", resourceID), r.URL.String(), "URL is correct") + _, ok := r.Header["Servicekey"] + assert.Equal(false, ok, "servicekey header is not set") + key, ok := r.Header["Authorization"] + assert.Equal(true, ok, "Authorization header is set") + assert.Equal("Bearer testtoken", key[0], "Authorization header is correct") + key, ok = r.Header["Cloud-Resource-Name"] + assert.Equal(true, ok, "cloud-resource-name header is set") + assert.Equal("testibmcrn", key[0], "cloud_resource_name header is correct") + key = r.Header["Content-Type"] + assert.Equal("application/json", key[0], "content-type header is correct") + })) + defer ts.Close() + + pc.baseURL = ts.URL + tempServiceKey := pc.serviceKey + pc.serviceKey = "" + pc.iamtoken = "testtoken" + pc.cloud_resource_name = "testibmcrn" + + req := newRequestConfig( + &pc, + "GET", + fmt.Sprintf("/someapi/%s", resourceID), + testRequest{}, + ) + + _, err := req.MakeRequest() + assert.Nil(err, "No errors") + + pc.serviceKey = tempServiceKey + pc.iamtoken = "" + pc.cloud_resource_name = "" + }) + t.Run("Server receives proper method, URL, and headers for request without a body", func(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { assert.Equal("GET", r.Method, "method is correct") @@ -120,6 +158,39 @@ func TestRequest_MakeRequest(t *testing.T) { ) }) + t.Run("Reads and decodes response from the server, iamtoken", func(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + err := json.NewEncoder(w).Encode(viewResponse{ViewID: "test123456"}) + assert.Nil(err, "No errors") + })) + defer ts.Close() + + pc.baseURL = ts.URL + tempServiceKey := pc.serviceKey + pc.serviceKey = "" + pc.iamtoken = "testtoken" + pc.cloud_resource_name = "testibmcrn" + + req := newRequestConfig( + &pc, + "GET", + fmt.Sprintf("/someapi/%s", resourceID), + nil, + ) + + body, err := req.MakeRequest() + assert.Nil(err, "No errors") + assert.Equal( + `{"viewID":"test123456"}`, + strings.TrimSpace(string(body)), + "Returned body is correct", + ) + + pc.serviceKey = tempServiceKey + pc.iamtoken = "" + pc.cloud_resource_name = "" + }) + t.Run("Successfully marshals a provided body", func(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { postedBody, _ := io.ReadAll(r.Body) @@ -260,4 +331,30 @@ func TestRequest_MakeRequest(t *testing.T) { "Expected error message", ) }) + + t.Run("Returns error if servicekey and iamtoken are blank", func(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(400) + })) + defer ts.Close() + + pc.baseURL = ts.URL + pc.serviceKey = "" + pc.iamtoken = "" + + req := newRequestConfig( + &pc, + "POST", + fmt.Sprintf("/someapi/%s", resourceID), + nil, + ) + + _, err := req.MakeRequest() + assert.Error(err, "Expected error") + assert.Equal( + true, + strings.Contains(err.Error(), "expected either servicekey or iamtoken to be set"), + "Expected error message", + ) + }) }