From bdab4cbbbde34721638393693593819118f50189 Mon Sep 17 00:00:00 2001 From: Hunts Date: Fri, 12 Jul 2019 13:55:28 -0700 Subject: [PATCH 1/3] Add support for the new API Tokens auth scheme --- cloudflare.go | 34 ++++++++++++++++++++++++++++++---- cloudflare_test.go | 18 ++++++++++++++++++ errors.go | 1 + 3 files changed, 49 insertions(+), 4 deletions(-) diff --git a/cloudflare.go b/cloudflare.go index 241473af28b..3facc71fa00 100644 --- a/cloudflare.go +++ b/cloudflare.go @@ -20,11 +20,14 @@ import ( ) const apiURL = "https://api.cloudflare.com/client/v4" + const ( // AuthKeyEmail specifies that we should authenticate with API key and email address AuthKeyEmail = 1 << iota // AuthUserService specifies that we should authenticate with a User-Service key AuthUserService + // AuthToken specifies that we should authenticate with an API Token + AuthToken ) // API holds the configuration for the current API client. A client should not @@ -33,6 +36,7 @@ type API struct { APIKey string APIEmail string APIUserServiceKey string + APIToken string BaseURL string OrganizationID string UserAgent string @@ -92,6 +96,23 @@ func New(key, email string, opts ...Option) (*API, error) { return api, nil } +// NewWithAPIToken creates a new Cloudflare v4 API client using API Tokens +func NewWithAPIToken(token string, opts ...Option) (*API, error) { + if token == "" { + return nil, errors.New(errEmptyAPIToken) + } + + api, err := newClient(opts...) + if err != nil { + return nil, err + } + + api.APIToken = token + api.authType = AuthToken + + return api, nil +} + // NewWithUserServiceKey creates a new Cloudflare v4 API client using service key authentication. func NewWithUserServiceKey(key string, opts ...Option) (*API, error) { if key == "" { @@ -109,7 +130,7 @@ func NewWithUserServiceKey(key string, opts ...Option) (*API, error) { return api, nil } -// SetAuthType sets the authentication method (AuthyKeyEmail or AuthUserService). +// SetAuthType sets the authentication method (AuthKeyEmail, AuthToken, or AuthUserService). func (api *API) SetAuthType(authType int) { api.authType = authType } @@ -141,7 +162,7 @@ func (api *API) ZoneIDByName(zoneName string) (string, error) { } // makeRequest makes a HTTP request and returns the body as a byte slice, -// closing it before returnng. params will be serialized to JSON. +// closing it before returning. params will be serialized to JSON. func (api *API) makeRequest(method, uri string, params interface{}) ([]byte, error) { return api.makeRequestWithAuthType(context.TODO(), method, uri, params, api.authType) } @@ -185,8 +206,8 @@ func (api *API) makeRequestWithAuthTypeAndHeaders(ctx context.Context, method, u reqBody = bytes.NewReader(jsonBody) } if i > 0 { - // expect the backoff introduced here on errored requests to dominate the effect of rate limiting - // dont need a random component here as the rate limiter should do something similar + // expect the backoff introduced here on errorred requests to dominate the effect of rate limiting + // don't need a random component here as the rate limiter should do something similar // nb time duration could truncate an arbitrary float. Since our inputs are all ints, we should be ok sleepDuration := time.Duration(math.Pow(2, float64(i-1)) * float64(api.retryPolicy.MinRetryDelay)) @@ -277,6 +298,7 @@ func (api *API) request(ctx context.Context, method, uri string, reqBody io.Read copyHeader(combinedHeaders, api.headers) copyHeader(combinedHeaders, headers) req.Header = combinedHeaders + if authType&AuthKeyEmail != 0 { req.Header.Set("X-Auth-Key", api.APIKey) req.Header.Set("X-Auth-Email", api.APIEmail) @@ -284,6 +306,10 @@ func (api *API) request(ctx context.Context, method, uri string, reqBody io.Read if authType&AuthUserService != 0 { req.Header.Set("X-Auth-User-Service-Key", api.APIUserServiceKey) } + if authType&AuthToken != 0 { + req.Header.Set("Authorization", "Bearer "+api.APIToken) + } + if api.UserAgent != "" { req.Header.Set("User-Agent", api.UserAgent) } diff --git a/cloudflare_test.go b/cloudflare_test.go index 80ad841a12c..58da76395f1 100644 --- a/cloudflare_test.go +++ b/cloudflare_test.go @@ -72,6 +72,7 @@ func TestClient_Headers(t *testing.T) { assert.Equal(t, "GET", r.Method, "Expected method 'GET', got %s", r.Method) assert.Empty(t, r.Header.Get("X-Auth-Email")) assert.Empty(t, r.Header.Get("X-Auth-Key")) + assert.Empty(t, r.Header.Get("Authorization")) assert.Equal(t, "userservicekey", r.Header.Get("X-Auth-User-Service-Key")) assert.Equal(t, "application/json", r.Header.Get("Content-Type")) }) @@ -87,11 +88,28 @@ func TestClient_Headers(t *testing.T) { assert.Equal(t, "GET", r.Method, "Expected method 'GET', got %s", r.Method) assert.Empty(t, r.Header.Get("X-Auth-Email")) assert.Empty(t, r.Header.Get("X-Auth-Key")) + assert.Empty(t, r.Header.Get("Authorization")) assert.Equal(t, "userservicekey", r.Header.Get("X-Auth-User-Service-Key")) assert.Equal(t, "application/json", r.Header.Get("Content-Type")) }) client.UserDetails() teardown() + + // it should set Authorization and omit others credential headers when using NewWithAPIToken + setup() + client, err = NewWithAPIToken("my-api-token") + assert.NoError(t, err) + client.BaseURL = server.URL + mux.HandleFunc("/zones/123456", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "GET", r.Method, "Expected method 'GET', got %s", r.Method) + assert.Empty(t, r.Header.Get("X-Auth-Email")) + assert.Empty(t, r.Header.Get("X-Auth-Key")) + assert.Empty(t, r.Header.Get("X-Auth-User-Service-Key")) + assert.Equal(t, "Bearer my-api-token", r.Header.Get("Authorization")) + assert.Equal(t, "application/json", r.Header.Get("Content-Type")) + }) + client.UserDetails() + teardown() } func TestClient_RetryCanSucceedAfterErrors(t *testing.T) { diff --git a/errors.go b/errors.go index bc23631f34d..aa074989583 100644 --- a/errors.go +++ b/errors.go @@ -3,6 +3,7 @@ package cloudflare // Error messages const ( errEmptyCredentials = "invalid credentials: key & email must not be empty" + errEmptyAPIToken = "invalid credential: API Token must not be empty" errMakeRequestError = "error from makeRequest" errUnmarshalError = "error unmarshalling the JSON response" errRequestNotSuccessful = "error reported by API" From b73f5e8f51989ab478d672fa2c707e012a076f57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patryk=20Szczyg=C5=82owski?= Date: Mon, 15 Jul 2019 16:06:13 +0100 Subject: [PATCH 2/3] Update errors.go --- errors.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/errors.go b/errors.go index aa074989583..21c38b16801 100644 --- a/errors.go +++ b/errors.go @@ -3,7 +3,7 @@ package cloudflare // Error messages const ( errEmptyCredentials = "invalid credentials: key & email must not be empty" - errEmptyAPIToken = "invalid credential: API Token must not be empty" + errEmptyAPIToken = "invalid credentials: API Token must not be empty" errMakeRequestError = "error from makeRequest" errUnmarshalError = "error unmarshalling the JSON response" errRequestNotSuccessful = "error reported by API" From fa42d3604db26eb6fd7972a8d419fadde3ed0988 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patryk=20Szczyg=C5=82owski?= Date: Mon, 15 Jul 2019 16:07:33 +0100 Subject: [PATCH 3/3] Update cloudflare.go --- cloudflare.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloudflare.go b/cloudflare.go index 3facc71fa00..96aa29e4506 100644 --- a/cloudflare.go +++ b/cloudflare.go @@ -206,7 +206,7 @@ func (api *API) makeRequestWithAuthTypeAndHeaders(ctx context.Context, method, u reqBody = bytes.NewReader(jsonBody) } if i > 0 { - // expect the backoff introduced here on errorred requests to dominate the effect of rate limiting + // expect the backoff introduced here on errored requests to dominate the effect of rate limiting // don't need a random component here as the rate limiter should do something similar // nb time duration could truncate an arbitrary float. Since our inputs are all ints, we should be ok sleepDuration := time.Duration(math.Pow(2, float64(i-1)) * float64(api.retryPolicy.MinRetryDelay))