From a7c4fdcd6a2f585db815a7ff46b1e70e7d4b7ae7 Mon Sep 17 00:00:00 2001 From: Andrea Angiolillo Date: Fri, 11 Aug 2023 09:53:23 +0100 Subject: [PATCH] fix: How do I create Organization API Key with Organization Billing Admin permission and Project Read Only for projects (#1369) --- .github/workflows/acceptance-tests.yml | 6 +- ...ata_source_mongodbatlas_project_api_key.go | 2 +- ...ta_source_mongodbatlas_project_api_keys.go | 2 +- .../resource_mongodbatlas_project_api_key.go | 61 ++++++++++++--- ...ource_mongodbatlas_project_api_key_test.go | 75 +++++++++++++++++++ website/docs/r/project_api_key.html.markdown | 38 +++++++++- 6 files changed, 168 insertions(+), 16 deletions(-) diff --git a/.github/workflows/acceptance-tests.yml b/.github/workflows/acceptance-tests.yml index 4d82247d26..b15b424620 100644 --- a/.github/workflows/acceptance-tests.yml +++ b/.github/workflows/acceptance-tests.yml @@ -58,11 +58,13 @@ jobs: project: - 'mongodbatlas/data_source_mongodbatlas_project_invitation*.go' - 'mongodbatlas/data_source_mongodbatlas_project_ip_access_list*.go' - - 'mongodbatlas/data_source_mongodbatlas_project*.go' + - 'mongodbatlas/data_source_mongodbatlas_project.go' + - 'mongodbatlas/data_source_mongodbatlas_projects.go' - 'mongodbatlas/resource_mongodbatlas_access_list_api_key*.go' - 'mongodbatlas/resource_mongodbatlas_project_invitation*.go' - 'mongodbatlas/resource_mongodbatlas_project_ip_access_list*.go' - - 'mongodbatlas/resource_mongodbatlas_project*.go' + - 'mongodbatlas/resource_mongodbatlas_project.go' + - 'mongodbatlas/resource_mongodbatlas_project_test.go' serverless: - 'mongodbatlas/**_serverless**.go' network: diff --git a/mongodbatlas/data_source_mongodbatlas_project_api_key.go b/mongodbatlas/data_source_mongodbatlas_project_api_key.go index 5f5e60bf87..e0b61ee886 100644 --- a/mongodbatlas/data_source_mongodbatlas_project_api_key.go +++ b/mongodbatlas/data_source_mongodbatlas_project_api_key.go @@ -92,7 +92,7 @@ func dataSourceMongoDBAtlasProjectAPIKeyRead(ctx context.Context, d *schema.Reso return diag.FromErr(fmt.Errorf("error setting `private_key`: %s", err)) } - if projectAssignments, err := newProjectAssignment(ctx, conn, apiKeyID); err == nil { + if projectAssignments, err := newProjectAssignment(ctx, conn, projectID, apiKeyID); err == nil { if err := d.Set("project_assignment", projectAssignments); err != nil { return diag.Errorf(errorProjectSetting, `project_assignment`, projectID, err) } diff --git a/mongodbatlas/data_source_mongodbatlas_project_api_keys.go b/mongodbatlas/data_source_mongodbatlas_project_api_keys.go index 722e646bf5..9b23c467a6 100644 --- a/mongodbatlas/data_source_mongodbatlas_project_api_keys.go +++ b/mongodbatlas/data_source_mongodbatlas_project_api_keys.go @@ -128,7 +128,7 @@ func flattenProjectAPIKeys(ctx context.Context, conn *matlas.Client, projectID s "role_names": flattenProjectAPIKeyRoles(projectID, apiKey.Roles), } - projectAssignment, err := newProjectAssignment(ctx, conn, apiKey.ID) + projectAssignment, err := newProjectAssignment(ctx, conn, projectID, apiKey.ID) if err != nil { return nil, err } diff --git a/mongodbatlas/resource_mongodbatlas_project_api_key.go b/mongodbatlas/resource_mongodbatlas_project_api_key.go index 43500dfb53..4f56b43bcc 100644 --- a/mongodbatlas/resource_mongodbatlas_project_api_key.go +++ b/mongodbatlas/resource_mongodbatlas_project_api_key.go @@ -13,6 +13,11 @@ import ( matlas "go.mongodb.org/atlas/mongodbatlas" ) +const ( + projectRolePrefix = "GROUP_" + orgReadOnlyRole = "ORG_READ_ONLY" +) + func resourceMongoDBAtlasProjectAPIKey() *schema.Resource { return &schema.Resource{ CreateContext: resourceMongoDBAtlasProjectAPIKeyCreate, @@ -78,7 +83,7 @@ func resourceMongoDBAtlasProjectAPIKey() *schema.Resource { } type APIProjectAssignmentKeyInput struct { - ProjectID string `json:"desc,omitempty"` + ProjectID string `json:"projectId,omitempty"` RoleNames []string `json:"roles,omitempty"` } @@ -96,6 +101,10 @@ func resourceMongoDBAtlasProjectAPIKeyCreate(ctx context.Context, d *schema.Reso projectAssignmentList := ExpandProjectAssignmentSet(projectAssignments.(*schema.Set)) for _, apiKeyList := range projectAssignmentList { if apiKeyList.ProjectID == projectID { + if err := apiKeyList.validateOrgKeyRoles(); err != nil { + return diag.FromErr(err) + } + createRequest.Roles = apiKeyList.RoleNames apiKey, resp, err = conn.ProjectAPIKeys.Create(ctx, projectID, createRequest) if err != nil { @@ -123,7 +132,6 @@ func resourceMongoDBAtlasProjectAPIKeyCreate(ctx context.Context, d *schema.Reso } } else { createRequest.Roles = expandStringList(d.Get("role_names").(*schema.Set).List()) - apiKey, resp, err = conn.ProjectAPIKeys.Create(ctx, projectID, createRequest) if err != nil { if resp != nil && resp.StatusCode == http.StatusNotFound { @@ -186,7 +194,7 @@ func resourceMongoDBAtlasProjectAPIKeyRead(ctx context.Context, d *schema.Resour if err := d.Set("role_names", nil); err != nil { return diag.FromErr(fmt.Errorf("error setting `roles`: %s", err)) } - if projectAssignments, err := newProjectAssignment(ctx, conn, apiKeyID); err == nil { + if projectAssignments, err := newProjectAssignment(ctx, conn, projectID, apiKeyID); err == nil { if err := d.Set("project_assignment", projectAssignments); err != nil { return diag.Errorf(errorProjectSetting, `created`, projectID, err) } @@ -317,7 +325,7 @@ func resourceMongoDBAtlasProjectAPIKeyDelete(ctx context.Context, d *schema.Reso return diag.FromErr(fmt.Errorf("error getting api key information: %s", err)) } - projectAssignments, err := getAPIProjectAssignments(ctx, conn, apiKeyOrgList, apiKeyID) + projectAssignments, err := getAPIProjectAssignments(ctx, conn, projectID, apiKeyOrgList, apiKeyID) if err != nil { return diag.FromErr(fmt.Errorf("error getting api key information: %s", err)) } @@ -386,7 +394,7 @@ func flattenProjectAPIKeyRoles(projectID string, apiKeyRoles []matlas.AtlasRole) flattenedOrgRoles := []string{} for _, role := range apiKeyRoles { - if strings.HasPrefix(role.RoleName, "GROUP_") && role.GroupID == projectID { + if role.GroupID == projectID { flattenedOrgRoles = append(flattenedOrgRoles, role.RoleName) } } @@ -408,13 +416,13 @@ func ExpandProjectAssignmentSet(projectAssignments *schema.Set) []*APIProjectAss return res } -func newProjectAssignment(ctx context.Context, conn *matlas.Client, apiKeyID string) ([]map[string]interface{}, error) { +func newProjectAssignment(ctx context.Context, conn *matlas.Client, projectID, apiKeyID string) ([]map[string]interface{}, error) { apiKeyOrgList, _, err := conn.Root.List(ctx, nil) if err != nil { return nil, fmt.Errorf("error getting api key information: %s", err) } - projectAssignments, err := getAPIProjectAssignments(ctx, conn, apiKeyOrgList, apiKeyID) + projectAssignments, err := getAPIProjectAssignments(ctx, conn, projectID, apiKeyOrgList, apiKeyID) if err != nil { return nil, fmt.Errorf("error getting api key information: %s", err) } @@ -467,7 +475,7 @@ func getStateProjectAssignmentAPIKeys(d *schema.ResourceData) (newAPIKeys, chang return } -func getAPIProjectAssignments(ctx context.Context, conn *matlas.Client, apiKeyOrgList *matlas.Root, apiKeyID string) ([]APIProjectAssignmentKeyInput, error) { +func getAPIProjectAssignments(ctx context.Context, conn *matlas.Client, projectIDUsedToCreateAPIKeys string, apiKeyOrgList *matlas.Root, apiKeyID string) ([]APIProjectAssignmentKeyInput, error) { projectAssignments := []APIProjectAssignmentKeyInput{} for idx, role := range apiKeyOrgList.APIKey.Roles { if strings.HasPrefix(role.RoleName, "ORG_") { @@ -475,17 +483,34 @@ func getAPIProjectAssignments(ctx context.Context, conn *matlas.Client, apiKeyOr if err != nil { return nil, fmt.Errorf("error getting api key information: %s", err) } + for _, val := range orgKeys { if val.ID == apiKeyID { for _, r := range val.Roles { temp := new(APIProjectAssignmentKeyInput) - if strings.HasPrefix(r.RoleName, "GROUP_") { + roles := map[string]string{} + if strings.HasPrefix(r.RoleName, projectRolePrefix) { temp.ProjectID = r.GroupID for _, l := range val.Roles { - if l.GroupID == temp.ProjectID { - temp.RoleNames = append(temp.RoleNames, l.RoleName) + if l.GroupID == temp.ProjectID || (l.GroupID == "" && temp.ProjectID == projectIDUsedToCreateAPIKeys) { + roles[l.RoleName] = l.RoleName } } + + tempRoleList := make([]string, 0, len(roles)) + for k := range roles { + if k == orgReadOnlyRole { + // When the user does not provide org roles + // the API key POST endpoing creates an org api key with + // the role ORG_READ_ONLY. We want to remove this from the state + // since the user did not provided it + continue + } + + tempRoleList = append(tempRoleList, k) + } + + temp.RoleNames = tempRoleList projectAssignments = append(projectAssignments, *temp) } } @@ -496,3 +521,17 @@ func getAPIProjectAssignments(ctx context.Context, conn *matlas.Client, apiKeyOr } return projectAssignments, nil } + +func (apiKey *APIProjectAssignmentKeyInput) validateOrgKeyRoles() error { + // When the user does not provide org roles + // the API key POST endpoing creates an org api key with + // the role ORG_READ_ONLY. We want to remove this from the state + // to avoid differences between config and state + for _, r := range apiKey.RoleNames { + if r == orgReadOnlyRole { + return fmt.Errorf(`%[1]s is not an allowed role for the resource. Remove %[1]s from the roles and run terraform apply again. Check out the resource documentation to know more`, orgReadOnlyRole) + } + } + + return nil +} diff --git a/mongodbatlas/resource_mongodbatlas_project_api_key_test.go b/mongodbatlas/resource_mongodbatlas_project_api_key_test.go index 06136b6ddc..15468423a6 100644 --- a/mongodbatlas/resource_mongodbatlas_project_api_key_test.go +++ b/mongodbatlas/resource_mongodbatlas_project_api_key_test.go @@ -175,6 +175,42 @@ func TestAccConfigRSProjectAPIKey_RecreateWhenDeletedExternally(t *testing.T) { }) } +func TestAccConfigRSProjectAPIKey_OrgRoles(t *testing.T) { + var ( + resourceName = "mongodbatlas_project_api_key.test" + dataSourceName = "data.mongodbatlas_project_api_key.test" + dataSourcesName = "data.mongodbatlas_project_api_keys.test" + orgID = os.Getenv("MONGODB_ATLAS_ORG_ID") + firstProjectName = acctest.RandomWithPrefix("test-acc") + secondProjectName = acctest.RandomWithPrefix("test-acc") + description = fmt.Sprintf("test-acc-project-api_key-%s", acctest.RandString(5)) + ) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheckBasic(t) }, + ProviderFactories: testAccProviderFactories, + CheckDestroy: testAccCheckMongoDBAtlasProjectAPIKeyDestroy, + Steps: []resource.TestStep{ + { + Config: testAccMongoDBAtlasProjectAPIKeyConfigOrgRoles(orgID, firstProjectName, secondProjectName, description), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet(resourceName, "project_id"), + resource.TestCheckResourceAttrSet(resourceName, "description"), + resource.TestCheckResourceAttr(resourceName, "description", description), + resource.TestCheckResourceAttrSet(resourceName, "project_assignment.0.project_id"), + resource.TestCheckResourceAttrSet(resourceName, "project_assignment.0.role_names.0"), + resource.TestCheckResourceAttrSet(dataSourceName, "project_assignment.0.project_id"), + resource.TestCheckResourceAttrSet(dataSourceName, "project_assignment.0.role_names.0"), + resource.TestCheckResourceAttrSet(dataSourceName, "project_id"), + resource.TestCheckResourceAttrSet(dataSourceName, "description"), + resource.TestCheckResourceAttrSet(dataSourcesName, "results.0.project_assignment.0.project_id"), + resource.TestCheckResourceAttrSet(dataSourcesName, "results.0.project_assignment.0.role_names.0"), + ), + }, + }, + }) +} + func deleteAPIKeyManually(orgID, descriptionPrefix string) error { conn := testAccProvider.Meta().(*MongoDBClient).Atlas list, _, err := conn.APIKeys.List(context.Background(), orgID, &matlas.ListOptions{}) @@ -268,3 +304,42 @@ func testAccMongoDBAtlasProjectAPIKeyConfigMultiple(orgID, projectName, descript `, orgID, projectName, description, roleNames) } + +func testAccMongoDBAtlasProjectAPIKeyConfigOrgRoles(orgID, firstProjectName, secondProjectName, description string) string { + return fmt.Sprintf(` + resource "mongodbatlas_project" "test" { + name = %[2]q + org_id = %[1]q + } + + resource "mongodbatlas_project" "testProject" { + name = %[3]q + org_id = %[1]q + } + + resource "mongodbatlas_project_api_key" "test" { + project_id = mongodbatlas_project.test.id + description = %[4]q + project_assignment { + project_id = mongodbatlas_project.test.id + role_names = ["ORG_BILLING_ADMIN", "GROUP_READ_ONLY"] + } + + project_assignment { + project_id = mongodbatlas_project.testProject.id + role_names = ["GROUP_OWNER"] + } + + } + + data "mongodbatlas_project_api_key" "test" { + project_id = mongodbatlas_project.test.id + api_key_id = mongodbatlas_project_api_key.test.api_key_id + } + + data "mongodbatlas_project_api_keys" "test" { + project_id = mongodbatlas_project.test.id + } + + `, orgID, firstProjectName, secondProjectName, description) +} diff --git a/website/docs/r/project_api_key.html.markdown b/website/docs/r/project_api_key.html.markdown index 8745790952..bf21d3a3c9 100644 --- a/website/docs/r/project_api_key.html.markdown +++ b/website/docs/r/project_api_key.html.markdown @@ -35,13 +35,43 @@ resource "mongodbatlas_project_api_key" "test" { } project_assignment { - project_id = "74259ee860c43338194b0f8e" + project_id = "64229ee820c42228194b0f4a" role_names = ["GROUP_READ_ONLY"] } } ``` +## Example Usage - Create Org PAK and Assign it to Multiple Projects + +```terraform +resource "mongodbatlas_project" "atlas-project" { + name = "ProjectTest" + org_id = "60ddf55c27a5a20955a707d7" +} + +resource "mongodbatlas_project_api_key" "api_1" { + description = "test api_key multi" + project_id = mongodbatlas_project.atlas-project.id + + // NOTE: The `project_id` of the first `project_assignment` element must be the same as the `project_id` of the resource. + project_assignment { + project_id = mongodbatlas_project.atlas-project.id + role_names = ["ORG_BILLING_ADMIN", "GROUP_READ_ONLY"] + } + + project_assignment { + project_id = "63dcfc256af00a5934e60924" + role_names = ["GROUP_READ_ONLY"] + } + + project_assignment { + project_id = "64c23af6f133166c39176cbf" + role_names = ["GROUP_OWNER"] + } +} +``` + ## Argument Reference * `project_id` -Unique 24-hexadecimal digit string that identifies your project. @@ -56,6 +86,12 @@ List of Project roles that the Programmatic API key needs to have. `project_assi * `project_id` - (Required) Project ID to assign to Access Key * `role_names` - (Required) List of Project roles that the Programmatic API key needs to have. Ensure you provide: at least one role and ensure all roles are valid for the Project. You must specify an array even if you are only associating a single role with the Programmatic API key. The [MongoDB Documentation](https://www.mongodb.com/docs/atlas/reference/user-roles/#project-roles) describes the valid roles that can be assigned. +~> **NOTE:** The `project_id` of the first `project_assignment` element must be the same as the `project_id` of the resource. + +~> **NOTE:** The organization level roles can be defined only in the first `project_assignment` element. + +~> **NOTE:** The `ORG_READ_ONLY` role at the organization level is invalid in this context. When the `project_assignment``` lacks organizational roles, the `mongodbatlas_project_api_key` resource generates an organization API key with the `ORG_READ_ONLY` role and associates it with `GROUP_*` roles. Consequently, the resource does not permit the use of `ORG_READ_ONLY` to ensure consistency between configuration and state. + ## Attributes Reference In addition to all arguments above, the following attributes are exported: