Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: How do I create Organization API Key with Organization Billing Admin permission and Project Read Only for projects #1369

Merged
merged 14 commits into from
Aug 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
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
}
Comment on lines +525 to +537
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

validating that the user did not provide ORG_READ_ONLY

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" {
maastha marked this conversation as resolved.
Show resolved Hide resolved
name = %[3]q
org_id = %[1]q
}

resource "mongodbatlas_project_api_key" "test" {
project_id = mongodbatlas_project.test.id
description = %[4]q
project_assignment {
maastha marked this conversation as resolved.
Show resolved Hide resolved
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 {
andreaangiolillo marked this conversation as resolved.
Show resolved Hide resolved
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.
andreaangiolillo marked this conversation as resolved.
Show resolved Hide resolved

~> **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.
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI


## Attributes Reference

In addition to all arguments above, the following attributes are exported:
Expand Down
Loading