Skip to content

Commit

Permalink
Implement organization management (RBAC) (#855)
Browse files Browse the repository at this point in the history
This resource allows managing the members of an organization:

- For each member their roles can be configured.
- Added members will be invited to the organization.
  - The user will still have to accept the invitation before they have access to the organization.
- Removed members will be removed from the organization
  • Loading branch information
gigerdo authored Sep 19, 2024
1 parent 75bf65d commit c155361
Show file tree
Hide file tree
Showing 24 changed files with 2,769 additions and 4 deletions.
3 changes: 3 additions & 0 deletions .changelog/855.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:feature
resource/organization: Allows managing the members of an organization: They can now be invited to the organization (and later removed) and their assigned roles can be updated.
```
120 changes: 120 additions & 0 deletions docs/resources/organization.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
---
page_title: "Elastic Cloud: ec_organization Resource"
description: |-
Manages an Elastic Cloud organization membership.
~> **This resource can only be used with Elastic Cloud SaaS**
---

# Resource: ec_organization

Manages an Elastic Cloud organization membership.

~> **This resource can only be used with Elastic Cloud SaaS**

## Example Usage

### Import

To import an organization into terraform, first define your organization configuration in your terraform file. For example:
```terraform
resource "ec_organization" "myorg" {
}
```

Then import the organization using your organization-id (The organization id can be found on [the organization page](https://cloud.elastic.co/account/members))
```bash
terraform import ec_organization.myorg <organization-id>
```

Now you can run `terraform plan` to see if there are any diffs between your config and how your organization is currently configured.

### Basic

```terraform
resource "ec_organization" "my_org" {
members = {
"[email protected]" = {
# All role definitions are optional
# Define roles for the whole organization
# Available roles are documented here: https://www.elastic.co/guide/en/cloud/current/ec-user-privileges.html#ec_organization_level_roles
organization_role = "billing-admin"
# Define deployment-specific roles
# Available roles are documented here: https://www.elastic.co/guide/en/cloud/current/ec-user-privileges.html#ec_instance_access_roles
deployment_roles = [
# A role can be given for all deployments
{
role = "editor"
all_deployments = true
},
# Or just for specific deployments
{
role = "editor"
deployment_ids = ["ce03a623751b4fc98d48400fec58b9c0"]
}
]
# Define roles for elasticsearch projects (Docs: https://www.elastic.co/docs/current/serverless/general/assign-user-roles#es)
project_elasticsearch_roles = [
# A role can be given for all projects
{
role = "admin"
all_projects = true
},
# Or just for specific projects
{
role = "admin"
project_ids = ["c866244b611442d585e23a0cc8c9434c"]
}
]
project_observability_roles = [
# Same as for an elasticsearch project
# Available roles are documented here: https://www.elastic.co/docs/current/serverless/general/assign-user-roles#observability
]
project_security_roles = [
# Same as for an elasticsearch project
# Available roles are documented here: https://www.elastic.co/docs/current/serverless/general/assign-user-roles#security
]
}
}
}
```

### Use variables to give the same roles to multiple users

```terraform
# To simplify managing multiple members with the same roles, the roles can be assigned to local variables
locals {
deployment_admin = {
deployment_roles = [
{
role = "admin"
all_deployments = true
}
]
}
deployment_viewer = {
deployment_roles = [
{
role = "viewer"
all_deployments = true
}
]
}
}
resource "ec_organization" "my_org" {
members = {
"[email protected]" = local.deployment_admin
"[email protected]" = local.deployment_viewer
"[email protected]" = local.deployment_viewer
}
}
```
2 changes: 1 addition & 1 deletion ec/ecdatasource/deploymentsdatasource/datasource_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ func Test_modelToState(t *testing.T) {
}

wantDeployments := modelV0{
ID: types.StringValue("2705093922"),
ID: types.StringValue("3192409966"),
NamePrefix: types.StringValue("test"),
ReturnCount: types.Int64Value(1),
DeploymentTemplateID: types.StringValue("azure-compute-optimized"),
Expand Down
60 changes: 60 additions & 0 deletions ec/ecresource/organizationresource/create.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// Licensed to Elasticsearch B.V. under one or more contributor
// license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright
// ownership. Elasticsearch B.V. licenses this file to you under
// the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.

package organizationresource

import (
"context"
"github.com/elastic/cloud-sdk-go/pkg/api/organizationapi"
"github.com/elastic/cloud-sdk-go/pkg/models"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/resource"
)

func (r *Resource) Create(ctx context.Context, request resource.CreateRequest, response *resource.CreateResponse) {
// It is not possible to create an organization, it already exists
// Instead, just import the already existing organization
response.Diagnostics.AddError("organization already exists", "please import the organization using terraform import")
}

func (r *Resource) createInvitation(ctx context.Context, email string, plan OrganizationMember, organizationID string, diagnostics *diag.Diagnostics) *OrganizationMember {
apiModel := modelToApi(ctx, plan, organizationID, diagnostics)
if diagnostics.HasError() {
return nil
}

invitations, err := organizationapi.CreateInvitation(organizationapi.CreateInvitationParams{
API: r.client,
OrganizationID: organizationID,
Emails: []string{email},
ExpiresIn: "7d",
RoleAssignments: apiModel.RoleAssignments,
})
if err != nil {
diagnostics.Append(diag.NewErrorDiagnostic("Failed to create invitation", err.Error()))
return nil
}

invitation := invitations.Invitations[0]
organizationMember := apiToModel(ctx, models.OrganizationMembership{
Email: *invitation.Email,
OrganizationID: invitation.Organization.ID,
RoleAssignments: invitation.RoleAssignments,
}, true, diagnostics)

return organizationMember
}
70 changes: 70 additions & 0 deletions ec/ecresource/organizationresource/delete.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// Licensed to Elasticsearch B.V. under one or more contributor
// license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright
// ownership. Elasticsearch B.V. licenses this file to you under
// the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.

package organizationresource

import (
"context"
"github.com/elastic/cloud-sdk-go/pkg/api/organizationapi"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/resource"
)

func (r *Resource) Delete(ctx context.Context, request resource.DeleteRequest, response *resource.DeleteResponse) {
// It is not possible to delete an organization
}

func (r *Resource) deleteMember(email string, member OrganizationMember, organizationID string, diags *diag.Diagnostics) {
if member.InvitationPending.ValueBool() {
r.deleteInvitation(email, organizationID, diags)
} else {
_, err := organizationapi.DeleteMember(organizationapi.DeleteMemberParams{
API: r.client,
OrganizationID: organizationID,
UserIDs: []string{member.UserID.ValueString()},
})
if err != nil {
diags.Append(diag.NewErrorDiagnostic("Removing organization member failed.", err.Error()))
return
}
}
}

func (r *Resource) deleteInvitation(email string, organizationID string, diags *diag.Diagnostics) {
invitations, err := organizationapi.ListInvitations(organizationapi.ListInvitationsParams{
API: r.client,
OrganizationID: organizationID,
})
if err != nil {
diags.Append(diag.NewErrorDiagnostic("Listing organization members failed", err.Error()))
return
}
for _, invitation := range invitations.Invitations {
if *invitation.Email == email {
_, err := organizationapi.DeleteInvitation(organizationapi.DeleteInvitationParams{
API: r.client,
OrganizationID: organizationID,
InvitationTokens: []string{*invitation.Token},
})
if err != nil {
diags.Append(diag.NewErrorDiagnostic("Removing member invitation failed", err.Error()))
return
}
return
}
}
}
34 changes: 34 additions & 0 deletions ec/ecresource/organizationresource/import.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Licensed to Elasticsearch B.V. under one or more contributor
// license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright
// ownership. Elasticsearch B.V. licenses this file to you under
// the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.

package organizationresource

import (
"context"
"github.com/hashicorp/terraform-plugin-framework/resource"
)

func (r *Resource) ImportState(ctx context.Context, request resource.ImportStateRequest, response *resource.ImportStateResponse) {
organizationID := request.ID

result := r.readFromApi(ctx, organizationID, &response.Diagnostics)
if response.Diagnostics.HasError() {
return
}

response.State.Set(ctx, &result)
}
43 changes: 43 additions & 0 deletions ec/ecresource/organizationresource/list_difference.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Licensed to Elasticsearch B.V. under one or more contributor
// license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright
// ownership. Elasticsearch B.V. licenses this file to you under
// the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.

package organizationresource

// Returns all the elements from array a that are not in array b
func difference[T interface{}](a, b []*T, getKey func(T) string) []*T {
var diff []*T
m := make(map[string]T)
for _, item := range b {
if item == nil {
continue
}
key := getKey(*item)
m[key] = *item
}

for _, item := range a {
if item == nil {
continue
}
key := getKey(*item)
if _, ok := m[key]; !ok {
diff = append(diff, item)
}
}

return diff
}
Loading

0 comments on commit c155361

Please sign in to comment.