Skip to content

Commit

Permalink
fix: How do I create Organization API Key with Organization Billing A…
Browse files Browse the repository at this point in the history
…dmin permission and Project Read Only for projects (#1369)
  • Loading branch information
andreaangiolillo authored Aug 11, 2023
1 parent 5185ff1 commit a7c4fdc
Show file tree
Hide file tree
Showing 6 changed files with 168 additions and 16 deletions.
6 changes: 4 additions & 2 deletions .github/workflows/acceptance-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion mongodbatlas/data_source_mongodbatlas_project_api_key.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
2 changes: 1 addition & 1 deletion mongodbatlas/data_source_mongodbatlas_project_api_keys.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
61 changes: 50 additions & 11 deletions mongodbatlas/resource_mongodbatlas_project_api_key.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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"`
}

Expand All @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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))
}
Expand Down Expand Up @@ -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)
}
}
Expand All @@ -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)
}
Expand Down Expand Up @@ -467,25 +475,42 @@ 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_") {
orgKeys, _, err := conn.APIKeys.List(ctx, apiKeyOrgList.APIKey.Roles[idx].OrgID, nil)
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)
}
}
Expand All @@ -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
}
75 changes: 75 additions & 0 deletions mongodbatlas/resource_mongodbatlas_project_api_key_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{})
Expand Down Expand Up @@ -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)
}
38 changes: 37 additions & 1 deletion website/docs/r/project_api_key.html.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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:
Expand Down

0 comments on commit a7c4fdc

Please sign in to comment.