diff --git a/README.md b/README.md index 676b60c..2687451 100644 --- a/README.md +++ b/README.md @@ -45,17 +45,30 @@ the plugin executable from source. To configure a role that uses an existing service ID: ```shell script - $ vault write ibmcloud/roles/myRole service_id=ServiceId-123456dbd-de02-4435-86ce-123456789abc - Success! Data written to: ibmcloud/roles/myRole + $ vault write ibmcloud/roles/myRole service_id=ServiceId-123456dbd-de02-4435-86ce-123456789abc + Success! Data written to: ibmcloud/roles/myRole ``` To configure a role that uses existing access groups: ```shell script - $ vault write ibmcloud/roles/myRole access_group_ids=AccessGroupId-43f12338-fc2c-41cd-b4f9-14eff0cbeb47,AccessGroupId-43f12111-fc2c-41cd-b4f9-14eff0cbeb21 - Success! Data written to: ibmcloud/roles/myRole + $ vault write ibmcloud/roles/myRole access_group_ids=AccessGroupId-43f12338-fc2c-41cd-b4f9-14eff0cbeb47,AccessGroupId-43f12111-fc2c-41cd-b4f9-14eff0cbeb21 + Success! Data written to: ibmcloud/roles/myRole ``` **There is a limit of 10 access groups per role.** +5. (Optional) Rotate the configured API key + + The API key provided in the initial configuration can be rotated. This creates a new API key in IBM Cloud, updates the secret engine configuration, + and deletes the currently configured API key from IBM Cloud. + + ```shell script + $ vault write -f ibmcloud/config/rotate-root + + Key Value + --- ----- + apikey_id ApiKey-2a3984a6-f855-4c69-893d-491d32228c17 + ``` + ## Usage After the secrets engine is configured and a user/machine has a Vault token with the proper permission, it can generate credentials. @@ -164,6 +177,38 @@ $ curl \ } ``` +## Rotate Root Credentials + +Rotates the IBM Cloud API key used by Vault for this mount. A new key will be generated +for same user or service ID and account as the existing API key. The configuration is updated +and then the old API key is deleted. + +The ID of the new API key is returned in the response. + + +| Method | Path | +|----------|-------------------------------------------------| +| `POST` | `/ibmcloud/config/rotate-root` | + + +### Sample Request +```shell script +$ curl \ + --header "X-Vault-Token: ..." \ + --request POST \ + https://127.0.0.1:8200/v1/ibmcloud/config/rotate-root +``` + +### Sample Response +```json +{ + "data": { + "api_key_id": "ApiKey-0abbbbbb-21cc-4dcc-a9cc-b59bc15c7aa1" + }, + "...": "..." +} +``` + ## Delete Config Deletes the previously configured configuration and clears the configured credentials in the plugin. diff --git a/backend.go b/backend.go index b7a6299..45661c1 100644 --- a/backend.go +++ b/backend.go @@ -3,6 +3,7 @@ package ibmcloudsecrets import ( "context" "errors" + "fmt" "strings" "sync" "time" @@ -43,6 +44,7 @@ func backend(c *logical.BackendConfig) *ibmCloudSecretBackend { Paths: framework.PathAppend( []*framework.Path{ pathConfig(b), + pathConfigRotateRoot(b), pathSecretServiceIDKey(b), }, pathsRoles(b), @@ -128,6 +130,20 @@ func (b *ibmCloudSecretBackend) getAdminToken(ctx context.Context, s logical.Sto if resp != nil { return "", resp.Error() } + + // Verify the configured admin API key is for the same account that is configured for the engine + apiKeyDetails, err := iam.GetAPIKeyDetails(token, config.APIKey) + if err != nil { + b.Logger().Error("error obtaining details about the configured admin API key", "error", err) + return "", err + } + + if apiKeyDetails.AccountID != config.Account { + err = fmt.Errorf("error: the account of the configured API key, %s, does not match the account in the configuration: %s", apiKeyDetails.AccountID, config.Account) + b.Logger().Error("error: configuration account mismatch", "error", err) + return "", err + } + b.adminToken = token b.adminTokenExpiry = adminTokenInfo.Expiry return b.adminToken, nil diff --git a/iam_helper.go b/iam_helper.go index 7b559b4..d323cf0 100644 --- a/iam_helper.go +++ b/iam_helper.go @@ -31,6 +31,7 @@ const ( getAccessGroup = "/v2/groups/%s" v1APIKeys = "/v1/apikeys" v1APIKeysID = v1APIKeys + "/%s" + v1APIKeyDetails = "/v1/apikeys/details" identity = "/identity" identityToken = "/identity/token" ) @@ -63,6 +64,12 @@ type APIKeyV1Response struct { ID string `json:"id"` } +type APIKeyDetailsResponse struct { + ID string `json:"id"` + IAMID string `json:"iam_id"` + AccountID string `json:"account_id"` +} + type iamHelper interface { ObtainToken(apiKey string) (string, error) VerifyToken(ctx context.Context, token string) (*tokenInfo, *logical.Response) @@ -71,8 +78,9 @@ type iamHelper interface { CreateServiceID(iamToken, accountID, roleName string) (iamID, identifier string, err error) DeleteServiceID(iamToken, identifier string) error AddServiceIDToAccessGroup(iamToken, iamID, group string) error - CreateAPIKey(iamToken, IAMid, accountID, roleName string) (*APIKeyV1Response, error) + CreateAPIKey(iamToken, IAMid, accountID, name, description string) (*APIKeyV1Response, error) DeleteAPIKey(iamToken, apiKeyID string) error + GetAPIKeyDetails(iamToken, apiKeyValue string) (*APIKeyDetailsResponse, error) Init(iamEndpoint string) Cleanup() } @@ -340,12 +348,12 @@ func (h *ibmCloudHelper) AddServiceIDToAccessGroup(iamToken string, iamID string return nil } -func (h *ibmCloudHelper) CreateAPIKey(iamToken, IAMid, accountID, roleName string) (*APIKeyV1Response, error) { +func (h *ibmCloudHelper) CreateAPIKey(iamToken, IAMid, accountID, name, description string) (*APIKeyV1Response, error) { requestBody, err := json.Marshal(map[string]interface{}{ - "name": fmt.Sprintf("vault-generated-%s", roleName), + "name": name, "iam_id": IAMid, "account_id": accountID, - "description": fmt.Sprintf("Generated by Vault's secret engine for IBM Cloud credentials using Vault role %s.", roleName), + "description": description, "store_value": false, }) if err != nil { @@ -403,6 +411,33 @@ func (h *ibmCloudHelper) DeleteAPIKey(iamToken, apiKeyID string) error { return nil } +func (h *ibmCloudHelper) GetAPIKeyDetails(iamToken, apiKeyValue string) (*APIKeyDetailsResponse, error) { + r, err := http.NewRequest(http.MethodGet, h.getURL(v1APIKeyDetails), nil) + if err != nil { + return nil, errwrap.Wrapf("failed creating http request: {{err}}", err) + } + + r.Header.Set("Authorization", "Bearer "+iamToken) + r.Header.Set("IAM-Apikey", apiKeyValue) + r.Header.Set("Content-Type", "application/json") + r.Header.Set("Accept", "application/json") + body, httpStatus, err := httpRequest(h.httpClient, r) + if err != nil { + return nil, err + } + + keyDetails := new(APIKeyDetailsResponse) + + if err := json.Unmarshal(body, &keyDetails); err != nil { + return nil, err + } + + if httpStatus != 200 { + return nil, fmt.Errorf("unexpected http status code: %v with response %v", httpStatus, string(body)) + } + return keyDetails, nil +} + func (h *ibmCloudHelper) DeleteServiceID(iamToken, identifier string) error { r, err := http.NewRequest(http.MethodDelete, h.getURL(serviceIDDetails, identifier), nil) if err != nil { diff --git a/mocks_test.go b/mocks_test.go index 514df5b..512754e 100644 --- a/mocks_test.go +++ b/mocks_test.go @@ -77,18 +77,18 @@ func (mr *MockiamHelperMockRecorder) Cleanup() *gomock.Call { } // CreateAPIKey mocks base method. -func (m *MockiamHelper) CreateAPIKey(iamToken, IAMid, accountID, roleName string) (*APIKeyV1Response, error) { +func (m *MockiamHelper) CreateAPIKey(iamToken, IAMid, accountID, name, description string) (*APIKeyV1Response, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CreateAPIKey", iamToken, IAMid, accountID, roleName) + ret := m.ctrl.Call(m, "CreateAPIKey", iamToken, IAMid, accountID, name, description) ret0, _ := ret[0].(*APIKeyV1Response) ret1, _ := ret[1].(error) return ret0, ret1 } // CreateAPIKey indicates an expected call of CreateAPIKey. -func (mr *MockiamHelperMockRecorder) CreateAPIKey(iamToken, IAMid, accountID, roleName interface{}) *gomock.Call { +func (mr *MockiamHelperMockRecorder) CreateAPIKey(iamToken, IAMid, accountID, name, description interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateAPIKey", reflect.TypeOf((*MockiamHelper)(nil).CreateAPIKey), iamToken, IAMid, accountID, roleName) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateAPIKey", reflect.TypeOf((*MockiamHelper)(nil).CreateAPIKey), iamToken, IAMid, accountID, name, description) } // CreateServiceID mocks base method. @@ -135,6 +135,21 @@ func (mr *MockiamHelperMockRecorder) DeleteServiceID(iamToken, identifier interf return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteServiceID", reflect.TypeOf((*MockiamHelper)(nil).DeleteServiceID), iamToken, identifier) } +// GetAPIKeyDetails mocks base method. +func (m *MockiamHelper) GetAPIKeyDetails(iamToken, apiKeyValue string) (*APIKeyDetailsResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetAPIKeyDetails", iamToken, apiKeyValue) + ret0, _ := ret[0].(*APIKeyDetailsResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetAPIKeyDetails indicates an expected call of GetAPIKeyDetails. +func (mr *MockiamHelperMockRecorder) GetAPIKeyDetails(iamToken, apiKeyValue interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAPIKeyDetails", reflect.TypeOf((*MockiamHelper)(nil).GetAPIKeyDetails), iamToken, apiKeyValue) +} + // Init mocks base method. func (m *MockiamHelper) Init(iamEndpoint string) { m.ctrl.T.Helper() diff --git a/path_config_rotate_root.go b/path_config_rotate_root.go new file mode 100644 index 0000000..d85cf46 --- /dev/null +++ b/path_config_rotate_root.go @@ -0,0 +1,102 @@ +package ibmcloudsecrets + +import ( + "context" + "errors" + + "github.com/hashicorp/vault/sdk/framework" + "github.com/hashicorp/vault/sdk/logical" +) + +func pathConfigRotateRoot(b *ibmCloudSecretBackend) *framework.Path { + return &framework.Path{ + Pattern: "config/rotate-root", + + Operations: map[logical.Operation]framework.OperationHandler{ + logical.UpdateOperation: &framework.PathOperation{ + Callback: b.pathConfigRotateRootWrite, + }, + }, + + HelpSynopsis: pathConfigRotateRootHelpSyn, + HelpDescription: pathConfigRotateRootHelpDesc, + } +} + +func (b *ibmCloudSecretBackend) pathConfigRotateRootWrite(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + + config, errResp := b.getConfig(ctx, req.Storage) + if errResp != nil { + return errResp, nil + } + + if config == nil || config.APIKey == "" { + return nil, errors.New("no API key was set in the configuration") + } + + iam, resp := b.getIAMHelper(ctx, req.Storage) + if resp != nil { + b.Logger().Error("failed to retrieve an IAM helper", "error", resp.Error()) + return resp, nil + } + + adminToken, err := b.getAdminToken(ctx, req.Storage) + if err != nil { + b.Logger().Error("error obtaining the token for the configured API key", "error", err) + return nil, err + } + + oldKeyDetails, err := iam.GetAPIKeyDetails(adminToken, config.APIKey) + if err != nil { + b.Logger().Error("error obtaining details about the current API key", "error", err) + return nil, err + } + + // with old key, verify account == acount from the config + keyName := "vault-generated-root-credential" + keyDescription := "Generated by Vault's secret engine for IBM Cloud credentials during root key rotation." + + // Generate a new service account key + newAPIKey, err := iam.CreateAPIKey(adminToken, oldKeyDetails.IAMID, oldKeyDetails.AccountID, keyName, keyDescription) + if err != nil { + return nil, err + } + + // Update the configuration with the new key + config.APIKey = newAPIKey.APIKey + entry, err := logical.StorageEntryJSON("config", config) + if err != nil { + return nil, err + } + if err := req.Storage.Put(ctx, entry); err != nil { + return nil, err + } + + // Reset the backend to pick up the new key + b.reset() + + // Delete the old API key + err = iam.DeleteAPIKey(adminToken, oldKeyDetails.ID) + if err != nil { + errResponse := logical.ErrorResponse("error deleting API key %s after successfully rotating the API key to key %s: %s", oldKeyDetails.ID, newAPIKey.ID, err) + return errResponse, err + } + + return &logical.Response{ + Data: map[string]interface{}{ + apiKeyID: newAPIKey.ID, + }, + }, nil +} + +const pathConfigRotateRootHelpSyn = ` +Request to rotate the IBM Cloud credentials used by Vault +` + +const pathConfigRotateRootHelpDesc = ` +This path attempts to rotate the IBM Cloud API key used by Vault +for this mount. It does this by generating a new key for the user or service ID, +replacing the internal value, and then deleting the old API key. +Note that it does not create a new service ID or user account, only a new +API key on the same IAM ID as the existing key. +` diff --git a/path_config_rotate_test.go b/path_config_rotate_test.go new file mode 100644 index 0000000..ffc4089 --- /dev/null +++ b/path_config_rotate_test.go @@ -0,0 +1,267 @@ +package ibmcloudsecrets + +import ( + "context" + "fmt" + "strings" + "testing" + "time" + + gomock "github.com/golang/mock/gomock" + "github.com/hashicorp/vault/sdk/logical" +) + +func TestConfigRotateRootSuccess(t *testing.T) { + t.Parallel() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockHelper := NewMockiamHelper(ctrl) + + // Set up the config, the iamHelper mocks, and the backend + var configData = map[string]interface{}{ + apiKeyField: "adminKey", + accountIDField: "theAccountID", + } + + mockHelper.EXPECT().ObtainToken("adminKey").Return("AdminToken", nil) + mockHelper.EXPECT().VerifyToken(gomock.Any(), "AdminToken").Return(&tokenInfo{Expiry: time.Now().Add(time.Hour)}, nil) + mockHelper.EXPECT().GetAPIKeyDetails("AdminToken", "adminKey"). + Return(&APIKeyDetailsResponse{ID: "oldID", IAMID: "testIAMID", AccountID: "theAccountID"}, nil) + + mockHelper.EXPECT().GetAPIKeyDetails("AdminToken", "adminKey"). + Return(&APIKeyDetailsResponse{ID: "oldID", IAMID: "testIAMID", AccountID: "theAccountID"}, nil) + mockHelper.EXPECT().CreateAPIKey("AdminToken", "testIAMID", "theAccountID", gomock.Any(), gomock.Any()). + Return(&APIKeyV1Response{APIKey: "newKeyVal", ID: "newKeyID"}, nil) + + mockHelper.EXPECT().Cleanup() + + mockHelper.EXPECT().DeleteAPIKey("AdminToken", "oldID").Return(nil) + + b, s := testBackend(t) + + err := testConfigCreate(t, b, s, configData) + if err != nil { + t.Fatal("error configuring the backend") + } + b.iamHelper = mockHelper + + // Rotate the key + resp, err := b.HandleRequest(context.Background(), &logical.Request{ + Operation: logical.UpdateOperation, + Path: "config/rotate-root", + Storage: s, + }) + if err != nil { + t.Fatalf("err: %v", err) + } + if resp != nil && resp.IsError() { + t.Fatalf("err: %v", resp.Error()) + } + + // verify the new API key ID was returned in the response + keyID, ok := resp.Data[apiKeyID] + if !ok { + t.Fatal("the api_key_id field was not found in the response") + } + + if keyID != "newKeyID" { + t.Fatal("the new API key ID was not the expected value in the response") + } + + // Verify the API key was updated in the config + config, resp := b.getConfig(context.Background(), s) + if resp != nil { + t.Fatalf("err: %v", resp.Error()) + } + + if config.APIKey != "newKeyVal" { + t.Fatalf("the API key was not set as expected. Received %s. Expected %s", config.APIKey, "newKeyVal") + } +} + +func TestRotateCreateFails(t *testing.T) { + // Test the case when creating the new API key fails + + t.Parallel() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockHelper := NewMockiamHelper(ctrl) + + // Set up the config, the iamHelper mocks, and the backend + var configData = map[string]interface{}{ + apiKeyField: "adminKey", + accountIDField: "theAccountID", + } + + mockHelper.EXPECT().ObtainToken("adminKey").Return("AdminToken", nil) + mockHelper.EXPECT().VerifyToken(gomock.Any(), "AdminToken").Return(&tokenInfo{Expiry: time.Now().Add(time.Hour)}, nil) + mockHelper.EXPECT().GetAPIKeyDetails("AdminToken", "adminKey"). + Return(&APIKeyDetailsResponse{ID: "oldID", IAMID: "testIAMID", AccountID: "theAccountID"}, nil) + + mockHelper.EXPECT().GetAPIKeyDetails("AdminToken", "adminKey"). + Return(&APIKeyDetailsResponse{ID: "oldID", IAMID: "testIAMID", AccountID: "theAccountID"}, nil) + mockHelper.EXPECT().CreateAPIKey("AdminToken", "testIAMID", "theAccountID", gomock.Any(), gomock.Any()). + Return(nil, fmt.Errorf("intentional CreateAPIKey mock failure")) + + b, s := testBackend(t) + + err := testConfigCreate(t, b, s, configData) + if err != nil { + t.Fatal("error configuring the backend") + } + b.iamHelper = mockHelper + + // Rotate the key + resp, err := b.HandleRequest(context.Background(), &logical.Request{ + Operation: logical.UpdateOperation, + Path: "config/rotate-root", + Storage: s, + }) + + // Verify the expected create failure is received + if resp != nil { + t.Fatalf("error: received a response when none was expected: %v", resp) + } + + if err == nil { + t.Fatalf("error: did not receive an error from the rotation as expected") + } + + if err.Error() != "intentional CreateAPIKey mock failure" { + t.Fatalf("error: did not receive the expected error message. Received this instead %s", err.Error()) + } + + // Verify the API key was not updated in the config + config, resp := b.getConfig(context.Background(), s) + if resp != nil { + t.Fatalf("err: %v", resp.Error()) + } + + if config.APIKey != "adminKey" { + t.Fatalf("the API key was no set as expected. Received %s. Expected %s", config.APIKey, "adminKey") + } +} + +func TestRotateDeleteKeyFails(t *testing.T) { + // Test the case where the key is rotated but the deletion of the old key fails + t.Parallel() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockHelper := NewMockiamHelper(ctrl) + + // Set up the config, the iamHelper mocks, and the backend + var configData = map[string]interface{}{ + apiKeyField: "adminKey", + accountIDField: "theAccountID", + } + + mockHelper.EXPECT().ObtainToken("adminKey").Return("AdminToken", nil) + mockHelper.EXPECT().VerifyToken(gomock.Any(), "AdminToken").Return(&tokenInfo{Expiry: time.Now().Add(time.Hour)}, nil) + mockHelper.EXPECT().GetAPIKeyDetails("AdminToken", "adminKey"). + Return(&APIKeyDetailsResponse{ID: "oldID", IAMID: "testIAMID", AccountID: "theAccountID"}, nil) + + mockHelper.EXPECT().GetAPIKeyDetails("AdminToken", "adminKey"). + Return(&APIKeyDetailsResponse{ID: "oldID", IAMID: "testIAMID", AccountID: "theAccountID"}, nil) + mockHelper.EXPECT().CreateAPIKey("AdminToken", "testIAMID", "theAccountID", gomock.Any(), gomock.Any()). + Return(&APIKeyV1Response{APIKey: "newKeyVal", ID: "newKeyID"}, nil) + + mockHelper.EXPECT().Cleanup() + + mockHelper.EXPECT().DeleteAPIKey("AdminToken", "oldID"). + Return(fmt.Errorf("intentional DeleteAPIKey mock failure")) + + b, s := testBackend(t) + + err := testConfigCreate(t, b, s, configData) + if err != nil { + t.Fatal("error configuring the backend") + } + b.iamHelper = mockHelper + + // Rotate the key + resp, err := b.HandleRequest(context.Background(), &logical.Request{ + Operation: logical.UpdateOperation, + Path: "config/rotate-root", + Storage: s, + }) + // Verify the expected create failure is received + if resp == nil { + t.Fatalf("error: a response was expected, but none was received") + } + + if err == nil { + t.Fatalf("error: did not receive an error from the rotation as expected") + } + + if err.Error() != "intentional DeleteAPIKey mock failure" { + t.Fatalf("error: did not receive the expected error message. Received this instead %s", err.Error()) + } + + // verify the response error has both the old and new key IDs in it + if !strings.Contains(resp.Error().Error(), "oldID") { + t.Fatalf("expected %s to be in error %v", "oldID", resp.Error()) + } + if !strings.Contains(resp.Error().Error(), "newKeyID") { + t.Fatalf("expected %s to be in error %v", "newKeyID", resp.Error()) + } + + // Verify the API key was updated in the config + config, resp := b.getConfig(context.Background(), s) + if resp != nil { + t.Fatalf("err: %v", resp.Error()) + } + + if config.APIKey != "newKeyVal" { + t.Fatalf("the API key was not set as expected. Received %s. Expected %s", config.APIKey, "newKeyVal") + } +} + +func TestConfigRotateNoConfigSet(t *testing.T) { + // Test with no config set + b, s := testBackend(t) + + // Rotate the key + resp, err := b.HandleRequest(context.Background(), &logical.Request{ + Operation: logical.UpdateOperation, + Path: "config/rotate-root", + Storage: s, + }) + if err != nil { + t.Fatal(err) + } + expectedMsg := "no API key was set in the configuration" + if resp == nil { + t.Fatalf("expected an error response on rotation but did not receive one") + } else if !strings.Contains(resp.Error().Error(), expectedMsg) { + t.Fatalf("expected message \"%s\" to be in error: %v", expectedMsg, resp.Error()) + } + + // Test when the key is set to the empty string + var configData = map[string]interface{}{ + apiKeyField: "", + accountIDField: "", + } + err = testConfigCreate(t, b, s, configData) + if err != nil { + t.Fatal("error configuring the backend") + } + + // Rotate the key + resp, err = b.HandleRequest(context.Background(), &logical.Request{ + Operation: logical.UpdateOperation, + Path: "config/rotate-root", + Storage: s, + }) + if err != nil { + t.Fatal(err) + } + + if resp == nil { + t.Fatalf("expected an error response on rotation but did not receive one") + } else if !strings.Contains(resp.Error().Error(), expectedMsg) { + t.Fatalf("expected message \"%s\" to be in error: %v", expectedMsg, resp.Error()) + } +} diff --git a/path_creds.go b/path_creds.go index 4fb817d..ea5aa1e 100644 --- a/path_creds.go +++ b/path_creds.go @@ -209,8 +209,10 @@ func (b *ibmCloudSecretBackend) getSecretDynamicServiceID(ctx context.Context, s } } + keyName := fmt.Sprintf("vault-generated-%s", roleName) + keyDescription := fmt.Sprintf("Generated by Vault's secret engine for IBM Cloud credentials using Vault role %s.", roleName) // Create API key - apiKey, err := iam.CreateAPIKey(adminToken, iamID, config.Account, roleName) + apiKey, err := iam.CreateAPIKey(adminToken, iamID, config.Account, keyName, keyDescription) if err != nil { return nil, err } @@ -246,8 +248,11 @@ func (b *ibmCloudSecretBackend) getSecretStaticServiceID(ctx context.Context, s if resp != nil { return resp, nil } + + keyName := fmt.Sprintf("vault-generated-%s", roleName) + keyDescription := fmt.Sprintf("Generated by Vault's secret engine for IBM Cloud credentials using Vault role %s.", roleName) // Create API key - apiKey, err := iam.CreateAPIKey(adminToken, idInfo.IAMID, config.Account, roleName) + apiKey, err := iam.CreateAPIKey(adminToken, idInfo.IAMID, config.Account, keyName, keyDescription) if err != nil { return nil, err } diff --git a/path_creds_test.go b/path_creds_test.go index 3d124b9..a81e981 100644 --- a/path_creds_test.go +++ b/path_creds_test.go @@ -346,6 +346,8 @@ func getMockedBackendStaticServiceID(t *testing.T, ctrl *gomock.Controller, call // for the IBM Cloud API calls calls. mockHelper.EXPECT().ObtainToken("adminKey").Return(adminToken, nil) mockHelper.EXPECT().VerifyToken(gomock.Any(), adminToken).Return(&tokenInfo{Expiry: time.Now().Add(time.Hour)}, nil) + mockHelper.EXPECT().GetAPIKeyDetails("AdminToken", "adminKey"). + Return(&APIKeyDetailsResponse{ID: "oldID", IAMID: "testIAMID", AccountID: "theAccountID"}, nil) // Mock for create / update of the test role and the look up of the service ID's IAM ID mockHelper.EXPECT().CheckServiceIDAccount(adminToken, gomock.Any(), "theAccountID"). @@ -357,9 +359,17 @@ func getMockedBackendStaticServiceID(t *testing.T, ctrl *gomock.Controller, call return &serviceIDv1Response{ID: "serviceID1", IAMID: fmt.Sprintf("%s_iam", serviceID)}, nil }) - mockHelper.EXPECT().CreateAPIKey(adminToken, gomock.Any(), "theAccountID", "testRole"). + mockHelper.EXPECT().CreateAPIKey(adminToken, gomock.Any(), "theAccountID", gomock.Any(), gomock.Any()). Times(callCount["CreateAPIKey"]). - DoAndReturn(func(iamToken, iamID, accountID, roleName string) (*APIKeyV1Response, error) { + DoAndReturn(func(iamToken, iamID, accountID, name, description string) (*APIKeyV1Response, error) { + roleName := "testRole" + if !strings.Contains(name, roleName) { + t.Fatalf("expected %s to be in the key name %s", roleName, name) + } + if !strings.Contains(description, roleName) { + t.Fatalf("expected %s to be in the key description %s", roleName, description) + } + if iamID == "keyFailureGetUser_iam" { return nil, fmt.Errorf("intentional CreateAPIKey mock failure") } @@ -393,6 +403,8 @@ func getMockedBackendDynamicServiceID(t *testing.T, ctrl *gomock.Controller, cal // for the IBM Cloud API calls calls. mockHelper.EXPECT().ObtainToken("adminKey").Return(adminToken, nil) mockHelper.EXPECT().VerifyToken(gomock.Any(), adminToken).Return(&tokenInfo{Expiry: time.Now().Add(time.Hour)}, nil) + mockHelper.EXPECT().GetAPIKeyDetails("AdminToken", "adminKey"). + Return(&APIKeyDetailsResponse{ID: "oldID", IAMID: "testIAMID", AccountID: "theAccountID"}, nil) // Mock for create / update of the test role and the look up of the service ID's IAM ID mockHelper.EXPECT().VerifyAccessGroupExists(adminToken, gomock.Any(), "theAccountID"). @@ -424,15 +436,21 @@ func getMockedBackendDynamicServiceID(t *testing.T, ctrl *gomock.Controller, cal return nil }) - mockHelper.EXPECT().CreateAPIKey(adminToken, "createdServiceID_iam", "theAccountID", gomock.Any()). + mockHelper.EXPECT().CreateAPIKey(adminToken, "createdServiceID_iam", "theAccountID", gomock.Any(), gomock.Any()). Times(callCount["CreateAPIKey"]). - DoAndReturn(func(iamToken, iamID, accountID, roleName string) (*APIKeyV1Response, error) { - if roleName == "testRole" { + DoAndReturn(func(iamToken, iamID, accountID, name, description string) (*APIKeyV1Response, error) { + if strings.Contains(name, "testRole") { + if !strings.Contains(description, "testRole") { + t.Fatalf("expected %s to be in the key description %s", "testRole", description) + } return &APIKeyV1Response{ID: "apiKeyID", APIKey: "theAPIKey"}, nil - } else if roleName == "APIKeyErrorRole" { + } else if strings.Contains(name, "APIKeyErrorRole") { + if !strings.Contains(description, "APIKeyErrorRole") { + t.Fatalf("expected %s to be in the key description %s", "APIKeyErrorRole", description) + } return nil, fmt.Errorf("intentional test error from mock CreateAPIKey") } else { - return nil, fmt.Errorf("unexpected role name in CreateAPIKey: %s", roleName) + return nil, fmt.Errorf("unexpected key name in CreateAPIKey mock: %s", name) } }) @@ -462,6 +480,8 @@ func getMockedBackendStaticServiceIDDeleteTest(t *testing.T, ctrl *gomock.Contro // for the IBM Cloud API calls calls. mockHelper.EXPECT().ObtainToken("adminKey").Return(adminToken, nil) mockHelper.EXPECT().VerifyToken(gomock.Any(), adminToken).Return(&tokenInfo{Expiry: time.Now().Add(time.Hour)}, nil) + mockHelper.EXPECT().GetAPIKeyDetails("AdminToken", "adminKey"). + Return(&APIKeyDetailsResponse{ID: "oldID", IAMID: "testIAMID", AccountID: "theAccountID"}, nil) // Mock for create of the test role mockHelper.EXPECT().CheckServiceIDAccount(adminToken, gomock.Any(), "theAccountID"). diff --git a/path_roles_test.go b/path_roles_test.go index aefbf3d..f32fb3a 100644 --- a/path_roles_test.go +++ b/path_roles_test.go @@ -430,8 +430,9 @@ func getMockedBackend(t *testing.T, ctrl *gomock.Controller, minCalls map[string // For the adminKey we always return AdminToken, this lets enforce that the code is correctly using the admin token // for the IBM Cloud API calls calls. mockHelper.EXPECT().ObtainToken("adminKey").Return("AdminToken", nil) - mockHelper.EXPECT().VerifyToken(gomock.Any(), "AdminToken").Return(&tokenInfo{Expiry: time.Now().Add(time.Hour)}, nil) + mockHelper.EXPECT().GetAPIKeyDetails("AdminToken", "adminKey"). + Return(&APIKeyDetailsResponse{ID: "oldID", IAMID: "testIAMID", AccountID: "theAccountID"}, nil) mockHelper.EXPECT().VerifyAccessGroupExists("AdminToken", gomock.Any(), "theAccountID"). MinTimes(minCalls["VerifyAccessGroupExists"]).DoAndReturn(func(iamToken, group, accountID string) *logical.Response {