From 13162e70f07a41731e1abbd87bf7523d06815370 Mon Sep 17 00:00:00 2001 From: Eamonn O'Toole Date: Wed, 3 Apr 2024 13:17:22 +0100 Subject: [PATCH] Changes to support GLP IAM API Client In this PR we add support for GLP IAM API Clients. We retain support for GLCS API Clients. We also retain support for "non-api vended" GLCS API Clients for backwards compatibility. We've added a new provider parameter "iam_version" with the corresponding env-var HPEGL_IAM_VERSION to designate which version of IAM is being used: - glcs - glp The default value of this parameter is glp. We've added a function to validate the value of "iam_version". We've updated the "description" field for "api_vended_service_client" and "tenant_id" in light of the support for GLP IAM. The issuertoken package has been modified to generate the correct URL and the correct set of http request parameters depending on which type of IAM is being used. We've had to update various unit-tests and the generated mocks. --- go.mod | 2 +- pkg/mocks/IdentityAPI_mocks.go | 8 +- pkg/provider/provider.go | 49 +++++++++++-- pkg/provider/provider_test.go | 43 ++++++++++- pkg/token/httpclient/httpclient.go | 7 +- pkg/token/httpclient/httpclient_test.go | 12 +-- pkg/token/issuertoken/issuertoken.go | 46 +++++++++--- pkg/token/issuertoken/issuertoken_test.go | 89 +++++++++++++++++++++++ pkg/token/serviceclient/handler.go | 11 ++- pkg/token/serviceclient/handler_test.go | 4 +- 10 files changed, 237 insertions(+), 34 deletions(-) create mode 100644 pkg/token/issuertoken/issuertoken_test.go diff --git a/go.mod b/go.mod index 34c89bb..b2b26c7 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/hewlettpackard/hpegl-provider-lib go 1.18 require ( + github.com/davecgh/go-spew v1.1.1 github.com/golang/mock v1.6.0 github.com/golangci/golangci-lint v1.43.0 github.com/hashicorp/terraform-plugin-sdk/v2 v2.24.1 @@ -35,7 +36,6 @@ require ( github.com/charithe/durationcheck v0.0.9 // indirect github.com/chavacava/garif v0.0.0-20221024190013-b3ef35877348 // indirect github.com/daixiang0/gci v0.9.0 // indirect - github.com/davecgh/go-spew v1.1.1 // indirect github.com/denis-tingajkin/go-header v0.4.2 // indirect github.com/esimonov/ifshort v1.0.4 // indirect github.com/ettle/strcase v0.1.1 // indirect diff --git a/pkg/mocks/IdentityAPI_mocks.go b/pkg/mocks/IdentityAPI_mocks.go index a939148..2a95b86 100644 --- a/pkg/mocks/IdentityAPI_mocks.go +++ b/pkg/mocks/IdentityAPI_mocks.go @@ -35,16 +35,16 @@ func (m *MockIdentityAPI) EXPECT() *MockIdentityAPIMockRecorder { } // GenerateToken mocks base method. -func (m *MockIdentityAPI) GenerateToken(arg0 context.Context, arg1, arg2, arg3 string) (string, error) { +func (m *MockIdentityAPI) GenerateToken(arg0 context.Context, arg1, arg2, arg3, arg4 string) (string, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GenerateToken", arg0, arg1, arg2, arg3) + ret := m.ctrl.Call(m, "GenerateToken", arg0, arg1, arg2, arg3, arg4) ret0, _ := ret[0].(string) ret1, _ := ret[1].(error) return ret0, ret1 } // GenerateToken indicates an expected call of GenerateToken. -func (mr *MockIdentityAPIMockRecorder) GenerateToken(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { +func (mr *MockIdentityAPIMockRecorder) GenerateToken(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GenerateToken", reflect.TypeOf((*MockIdentityAPI)(nil).GenerateToken), arg0, arg1, arg2, arg3) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GenerateToken", reflect.TypeOf((*MockIdentityAPI)(nil).GenerateToken), arg0, arg1, arg2, arg3, arg4) } diff --git a/pkg/provider/provider.go b/pkg/provider/provider.go index 87ef0a8..f48070b 100644 --- a/pkg/provider/provider.go +++ b/pkg/provider/provider.go @@ -1,4 +1,4 @@ -// (C) Copyright 2021 Hewlett Packard Enterprise Development LP +// (C) Copyright 2021-2024 Hewlett Packard Enterprise Development LP package provider @@ -11,6 +11,16 @@ import ( "github.com/hewlettpackard/hpegl-provider-lib/pkg/registration" ) +const ( + // IAMVersionGLCS is the IAM version for GLCS + IAMVersionGLCS = "glcs" + // IAMVersionGLP is the IAM version for GLP + IAMVersionGLP = "glp" +) + +// Update this list with any new IAM versions +var iamVersionList = [...]string{IAMVersionGLCS, IAMVersionGLP} + // ConfigureFunc is a type definition of a function that returns a ConfigureContextFunc object // A function of this type is passed in to NewProviderFunc below type ConfigureFunc func(p *schema.Provider) schema.ConfigureContextFunc @@ -81,9 +91,18 @@ func Schema() map[string]*schema.Schema { Type: schema.TypeString, Optional: true, DefaultFunc: schema.EnvDefaultFunc("HPEGL_IAM_SERVICE_URL", "https://client.greenlake.hpe.com/api/iam"), - Description: `The IAM service URL to be used to generate tokens. In the case of API-vended API clients - (the default) then this should be set to the "issuer url" for the client. In the case of non-API-vended - API clients use the appropriate GL "client" URL. Can be set by HPEGL_IAM_SERVICE_URL env-var`, + Description: `The IAM service URL to be used to generate tokens. In the case of GLCS API clients + (the default) then this should be set to the "issuer url" for the client. In the case of GLP + API clients use the appropriate "Token URL" from the API screen. Can be set by HPEGL_IAM_SERVICE_URL env-var`, + } + + providerSchema["iam_version"] = &schema.Schema{ + Type: schema.TypeString, + Optional: true, + DefaultFunc: schema.EnvDefaultFunc("HPEGL_IAM_VERSION", IAMVersionGLCS), + ValidateFunc: ValidateIAMVersion, + Description: `The IAM version to be used. Can be set by HPEGL_IAM_VERSION env-var. Valid values are: + ` + fmt.Sprintf("%v", iamVersionList) + `The default is ` + IAMVersionGLCS + `.`, } providerSchema["api_vended_service_client"] = &schema.Schema{ @@ -98,7 +117,7 @@ func Schema() map[string]*schema.Schema { Type: schema.TypeString, Optional: true, DefaultFunc: schema.EnvDefaultFunc("HPEGL_TENANT_ID", ""), - Description: "The tenant-id to be used, can be set by HPEGL_TENANT_ID env-var", + Description: "The tenant-id to be used for GLCS IAM, can be set by HPEGL_TENANT_ID env-var", } providerSchema["user_id"] = &schema.Schema{ @@ -137,3 +156,23 @@ func convertToTypeSet(r *schema.Resource) *schema.Schema { Elem: r, } } + +// ValidateIAMVersion is a ValidateFunc for the "iam_version" field in the provider schema +func ValidateIAMVersion(v interface{}, k string) ([]string, []error) { + // check that v is in iamVersionList + found := false + for _, version := range iamVersionList { + if version == v.(string) { + found = true + break + } + } + + // add error if not found + es := make([]error, 0) + if !found { + es = append(es, fmt.Errorf("IAM version must be one of %v", iamVersionList)) + } + + return []string{}, es +} diff --git a/pkg/provider/provider_test.go b/pkg/provider/provider_test.go index 866d9ba..9beea4f 100644 --- a/pkg/provider/provider_test.go +++ b/pkg/provider/provider_test.go @@ -1,4 +1,4 @@ -// (C) Copyright 2021 Hewlett Packard Enterprise Development LP +// (C) Copyright 2021-2024 Hewlett Packard Enterprise Development LP package provider @@ -8,8 +8,9 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - "github.com/hewlettpackard/hpegl-provider-lib/pkg/registration" "github.com/stretchr/testify/assert" + + "github.com/hewlettpackard/hpegl-provider-lib/pkg/registration" ) func testResource() *schema.Resource { @@ -170,3 +171,41 @@ func TestNewProviderFunc(t *testing.T) { }) } } + +func TestValidateIAMVersion(t *testing.T) { + t.Parallel() + testcases := []struct { + name string + version string + hasError bool + }{ + { + name: "valid IAM version GLCS", + version: IAMVersionGLCS, + hasError: false, + }, + { + name: "valid IAM version GLP", + version: IAMVersionGLP, + hasError: false, + }, + { + name: "invalid IAM version", + version: "invalid", + hasError: true, + }, + } + + for _, testcase := range testcases { + tc := testcase + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + _, es := ValidateIAMVersion(tc.version, "iam_version") + if tc.hasError { + assert.NotEmpty(t, es) + } else { + assert.Empty(t, es) + } + }) + } +} diff --git a/pkg/token/httpclient/httpclient.go b/pkg/token/httpclient/httpclient.go index b1f696a..e9327bb 100644 --- a/pkg/token/httpclient/httpclient.go +++ b/pkg/token/httpclient/httpclient.go @@ -1,4 +1,4 @@ -// (C) Copyright 2021 Hewlett Packard Enterprise Development LP +// (C) Copyright 2021-2024 Hewlett Packard Enterprise Development LP package httpclient @@ -33,11 +33,12 @@ func New(identityServiceURL string, vendedServiceClient bool, passedInToken stri } } -func (c *Client) GenerateToken(ctx context.Context, tenantID, clientID, clientSecret string) (string, error) { +func (c *Client) GenerateToken(ctx context.Context, tenantID, clientID, clientSecret, iamVersion string) (string, error) { // we don't have a passed-in token, so we need to actually generate a token if c.passedInToken == "" { if c.vendedServiceClient { - token, err := issuertoken.GenerateToken(ctx, tenantID, clientID, clientSecret, c.identityServiceURL, c.httpClient) + token, err := issuertoken.GenerateToken( + ctx, clientID, clientSecret, c.identityServiceURL, iamVersion, c.httpClient) return token, err } diff --git a/pkg/token/httpclient/httpclient_test.go b/pkg/token/httpclient/httpclient_test.go index d76d94c..f31c1ce 100644 --- a/pkg/token/httpclient/httpclient_test.go +++ b/pkg/token/httpclient/httpclient_test.go @@ -1,4 +1,4 @@ -// (C) Copyright 2021 Hewlett Packard Enterprise Development LP +// (C) Copyright 2021-2024 Hewlett Packard Enterprise Development LP //nolint:structcheck package httpclient @@ -11,9 +11,11 @@ import ( "net/http" "testing" + "github.com/stretchr/testify/assert" + + "github.com/hewlettpackard/hpegl-provider-lib/pkg/provider" "github.com/hewlettpackard/hpegl-provider-lib/pkg/token/identitytoken" "github.com/hewlettpackard/hpegl-provider-lib/pkg/token/issuertoken" - "github.com/stretchr/testify/assert" ) type testCaseIssuer struct { @@ -205,7 +207,7 @@ func TestGenerateToken(t *testing.T) { c = createTestClient(tc.url, "", tc.statusCode, tc.token, true) - token, err := c.GenerateToken(tc.ctx, "", "", "") + token, err := c.GenerateToken(tc.ctx, "", "", "", provider.IAMVersionGLCS) if tc.err != nil { assert.EqualError(t, err, tc.err.Error()) } @@ -219,7 +221,7 @@ func TestGenerateToken(t *testing.T) { c = createTestClient(tc.url, "", tc.statusCode, tc.token, false) - token, err := c.GenerateToken(tc.ctx, "", "", "") + token, err := c.GenerateToken(tc.ctx, "", "", "", provider.IAMVersionGLCS) if tc.err != nil { assert.EqualError(t, err, tc.err.Error()) } @@ -232,7 +234,7 @@ func TestGenerateTokenPassedInToken(t *testing.T) { t.Parallel() c := createTestClient("", "testToken", http.StatusAccepted, nil, true) - token, err := c.GenerateToken(context.Background(), "", "", "") + token, err := c.GenerateToken(context.Background(), "", "", "", "") assert.Equal(t, "testToken", token) assert.NoError(t, err) } diff --git a/pkg/token/issuertoken/issuertoken.go b/pkg/token/issuertoken/issuertoken.go index 708bfec..aed65d7 100644 --- a/pkg/token/issuertoken/issuertoken.go +++ b/pkg/token/issuertoken/issuertoken.go @@ -1,4 +1,4 @@ -// (C) Copyright 2021-2023 Hewlett Packard Enterprise Development LP +// (C) Copyright 2021-2024 Hewlett Packard Enterprise Development LP package issuertoken @@ -11,6 +11,7 @@ import ( "net/url" "strings" + "github.com/hewlettpackard/hpegl-provider-lib/pkg/provider" tokenutil "github.com/hewlettpackard/hpegl-provider-lib/pkg/token/token-util" ) @@ -27,25 +28,26 @@ type TokenResponse struct { func GenerateToken( ctx context.Context, - tenantID, clientID, clientSecret string, identityServiceURL string, + iamVersion string, httpClient tokenutil.HttpClient, ) (string, error) { - params := url.Values{} - params.Add("client_id", clientID) - params.Add("client_secret", clientSecret) - params.Add("grant_type", "client_credentials") - params.Add("scope", "hpe-tenant") + // Generate the parameters and URL for the request + params, clientURL, err := generateParamsAndURL(clientID, clientSecret, identityServiceURL, iamVersion) + if err != nil { + return "", err + } - url := fmt.Sprintf("%s/v1/token", identityServiceURL) - req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, strings.NewReader(params.Encode())) + // Create the request + req, err := http.NewRequestWithContext(ctx, http.MethodPost, clientURL, strings.NewReader(params.Encode())) if err != nil { return "", err } req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + // Execute the request, with retries resp, err := tokenutil.DoRetries(func() (*http.Response, error) { return httpClient.Do(req) }, retryLimit) @@ -73,3 +75,29 @@ func GenerateToken( return token.AccessToken, nil } + +// generateParamsAndURL generates the parameters and URL for the request +func generateParamsAndURL(clientID, clientSecret, identityServiceURL, iamVersion string) (url.Values, string, error) { + params := url.Values{} + + // Add common parameters for an API Client + params.Add("client_id", clientID) + params.Add("client_secret", clientSecret) + params.Add("grant_type", "client_credentials") + + // Add specific parameters and generate URL for the IAM version + var clientURL string + switch iamVersion { + case provider.IAMVersionGLCS: + params.Add("scope", "hpe-tenant") + clientURL = fmt.Sprintf("%s/v1/token", identityServiceURL) + + case provider.IAMVersionGLP: + clientURL = identityServiceURL + + default: + return nil, "", fmt.Errorf("invalid IAM version") + } + + return params, clientURL, nil +} diff --git a/pkg/token/issuertoken/issuertoken_test.go b/pkg/token/issuertoken/issuertoken_test.go new file mode 100644 index 0000000..9f8b9e0 --- /dev/null +++ b/pkg/token/issuertoken/issuertoken_test.go @@ -0,0 +1,89 @@ +// (C) Copyright 2024 Hewlett Packard Enterprise Development LP + +package issuertoken + +import ( + "context" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/hewlettpackard/hpegl-provider-lib/pkg/provider" +) + +func TestGenerateToken(t *testing.T) { + t.Parallel() + testcases := []struct { + name string + iamVersion string + hasError bool + }{ + { + name: "valid IAM version GLCS", + iamVersion: provider.IAMVersionGLCS, + hasError: false, + }, + { + name: "valid IAM version GLP", + iamVersion: provider.IAMVersionGLP, + hasError: false, + }, + { + name: "invalid IAM version", + iamVersion: "invalid", + hasError: true, + }, + } + + for _, testcase := range testcases { + tc := testcase + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + _, err := GenerateToken(context.Background(), "clientID", "clientSecret", "identityServiceURL", tc.iamVersion, &http.Client{}) + if tc.hasError { + assert.NotNil(t, err) + } else { + assert.Nil(t, err) + } + }) + } +} + +func TestGenerateParamsAndURL(t *testing.T) { + t.Parallel() + testcases := []struct { + name string + iamVersion string + hasError bool + }{ + { + name: "valid IAM version GLCS", + iamVersion: provider.IAMVersionGLCS, + hasError: false, + }, + { + name: "valid IAM version GLP", + iamVersion: provider.IAMVersionGLP, + hasError: false, + }, + { + name: "invalid IAM version", + iamVersion: "invalid", + hasError: true, + }, + } + + for _, testcase := range testcases { + tc := testcase + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + _, _, err := generateParamsAndURL("clientID", "clientSecret", "identityServiceURL", tc.iamVersion) + if tc.hasError { + assert.NotNil(t, err) + } else { + assert.Nil(t, err) + } + }) + } +} diff --git a/pkg/token/serviceclient/handler.go b/pkg/token/serviceclient/handler.go index 6527a5b..a87dbe1 100644 --- a/pkg/token/serviceclient/handler.go +++ b/pkg/token/serviceclient/handler.go @@ -1,10 +1,11 @@ -// (C) Copyright 2021 Hewlett Packard Enterprise Development LP +// (C) Copyright 2021-2024 Hewlett Packard Enterprise Development LP package serviceclient import ( "context" "errors" + "github.com/davecgh/go-spew/spew" "net" "time" @@ -20,7 +21,7 @@ var _ common.TokenChannelInterface = (*Handler)(nil) //go:generate mockgen -build_flags=-mod=mod -destination=../../mocks/IdentityAPI_mocks.go -package=mocks github.com/hewlettpackard/hpegl-provider-lib/pkg/token/serviceclient IdentityAPI type IdentityAPI interface { - GenerateToken(context.Context, string, string, string) (string, error) + GenerateToken(context.Context, string, string, string, string) (string, error) } // Handler the handler for service-client creds @@ -30,6 +31,7 @@ type Handler struct { tenantID string clientID string clientSecret string + iamVersion string vendedServiceClient bool numRetries int client IdentityAPI @@ -54,12 +56,14 @@ type resourceData interface { // NewHandler creates a new handler and returns the common.TokenChannelInterface interface // Param resourceData can be *schema.ResourceData or any model which implements resourceData +// //nolint:forcetypeassert func NewHandler(d resourceData, opts ...CreateOpt) (common.TokenChannelInterface, error) { h := new(Handler) // set Handler fields h.iamServiceURL = d.Get("iam_service_url").(string) + h.iamVersion = d.Get("iam_version").(string) h.tenantID = d.Get("tenant_id").(string) h.clientID = d.Get("user_id").(string) h.clientSecret = d.Get("user_secret").(string) @@ -149,6 +153,7 @@ func (h *Handler) retrieveToken() common.Result { } // If token is about to expire in TimeToTokenExpiry seconds or less generate a new one + spew.Dump(tokenDetails) if tokenDetails.Expiry-now <= common.TimeToTokenExpiry { token, retry, err := h.generateToken() if retry { @@ -178,7 +183,7 @@ func (h *Handler) generateToken() (string, bool, error) { var err error // TODO pass a context down to here - token, err = h.client.GenerateToken(context.Background(), h.tenantID, h.clientID, h.clientSecret) + token, err = h.client.GenerateToken(context.Background(), h.tenantID, h.clientID, h.clientSecret, h.iamVersion) // If this is a retryable error check to see if we've reached our retryLimit or not, if we can retry again // return true diff --git a/pkg/token/serviceclient/handler_test.go b/pkg/token/serviceclient/handler_test.go index 5c4ca37..1401b3a 100644 --- a/pkg/token/serviceclient/handler_test.go +++ b/pkg/token/serviceclient/handler_test.go @@ -1,4 +1,4 @@ -// (C) Copyright 2021-2023 Hewlett Packard Enterprise Development LP +// (C) Copyright 2021-2024 Hewlett Packard Enterprise Development LP package serviceclient_test @@ -103,7 +103,7 @@ func TestHandler(t *testing.T) { assert.NoError(t, err) testToken := generateTestToken(600) - mock.EXPECT().GenerateToken(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(testToken, tc.err).MaxTimes(8) + mock.EXPECT().GenerateToken(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(testToken, tc.err).MaxTimes(8) handler, err := serviceclient.NewHandler(d, serviceclient.WithIdentityAPI(mock)) assert.NoError(t, err)