Skip to content

Commit

Permalink
Add support for user group resources (#473)
Browse files Browse the repository at this point in the history
* feat: add support for user group resources

Signed-off-by: Michal Wasilewski <[email protected]>
  • Loading branch information
mwasilew2 authored Oct 9, 2023
1 parent 2b66265 commit 1e0ed0c
Show file tree
Hide file tree
Showing 7 changed files with 362 additions and 0 deletions.
47 changes: 47 additions & 0 deletions docs/resources/idp_group_mapping.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
---
# generated by https://github.com/hashicorp/terraform-plugin-docs
page_title: "spacelift_idp_group_mapping Resource - terraform-provider-spacelift"
subcategory: ""
description: |-
spacelift_idp_group_mapping represents a mapping (binding) between a user group (as provided by IdP) and a Spacelift User Management Policy. If you assign permissions (a Policy) to a user group, all users in the group will have those permissions unless the user's permissions are higher than the group's permissions.
---

# spacelift_idp_group_mapping (Resource)

`spacelift_idp_group_mapping` represents a mapping (binding) between a user group (as provided by IdP) and a Spacelift User Management Policy. If you assign permissions (a Policy) to a user group, all users in the group will have those permissions unless the user's permissions are higher than the group's permissions.

## Example Usage

```terraform
resource "spacelift_idp_group_mapping" "test" {
name = "test"
policy {
space_id = "root"
role = "ADMIN"
}
policy {
space_id = "legacy"
role = "ADMIN"
}
}
```

<!-- schema generated by tfplugindocs -->
## Schema

### Required

- `name` (String) Name of the user group - should be unique in one account
- `policy` (Block List, Min: 1) (see [below for nested schema](#nestedblock--policy))

### Read-Only

- `id` (String) The ID of this resource.

<a id="nestedblock--policy"></a>
### Nested Schema for `policy`

Required:

- `role` (String) Type of access to the space. Possible values are: READ, WRITE, ADMIN
- `space_id` (String) ID (slug) of the space the user group has access to
11 changes: 11 additions & 0 deletions examples/resources/spacelift_idp_group_mapping/resource.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
resource "spacelift_idp_group_mapping" "test" {
name = "test"
policy {
space_id = "root"
role = "ADMIN"
}
policy {
space_id = "legacy"
role = "ADMIN"
}
}
12 changes: 12 additions & 0 deletions spacelift/internal/structs/user_group.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package structs

type SpaceAccessRule struct {
Space string `graphql:"space"`
SpaceAccessLevel string `graphql:"spaceAccessLevel"`
}

type UserGroup struct {
ID string `graphql:"id"`
Name string `graphql:"groupName"`
AccessRules []SpaceAccessRule `graphql:"accessRules"`
}
20 changes: 20 additions & 0 deletions spacelift/internal/structs/user_group_input.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package structs

import "github.com/shurcooL/graphql"

type SpaceAccessLevel string

type SpaceAccessRuleInput struct {
Space graphql.ID `json:"space"`
SpaceAccessLevel SpaceAccessLevel `json:"spaceAccessLevel"`
}

type ManagedUserGroupCreateInput struct {
Name graphql.String `json:"groupName"`
AccessRules []SpaceAccessRuleInput `json:"accessRules"`
}

type ManagedUserGroupUpdateInput struct {
ID graphql.ID `json:"id"`
AccessRules []SpaceAccessRuleInput `json:"accessRules"`
}
1 change: 1 addition & 0 deletions spacelift/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ func Provider(commit, version string) plugin.ProviderFunc {
"spacelift_stack_aws_role": resourceStackAWSRole(), // deprecated
"spacelift_stack_gcp_service_account": resourceStackGCPServiceAccount(), // deprecated
"spacelift_terraform_provider": resourceTerraformProvider(),
"spacelift_idp_group_mapping": resourceIdpGroupMapping(),
"spacelift_vcs_agent_pool": resourceVCSAgentPool(),
"spacelift_webhook": resourceWebhook(),
"spacelift_named_webhook": resourceNamedWebhook(),
Expand Down
174 changes: 174 additions & 0 deletions spacelift/resource_idp_group_mapping.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
package spacelift

import (
"context"

"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
"github.com/shurcooL/graphql"

"github.com/spacelift-io/terraform-provider-spacelift/spacelift/internal"
"github.com/spacelift-io/terraform-provider-spacelift/spacelift/internal/structs"
"github.com/spacelift-io/terraform-provider-spacelift/spacelift/internal/validations"
)

var validAccessLevels = []string{
"READ",
"WRITE",
"ADMIN",
}

func resourceIdpGroupMapping() *schema.Resource {
return &schema.Resource{
Description: "" +
"`spacelift_idp_group_mapping` represents a mapping (binding) between a user group (as provided by IdP) " +
"and a Spacelift User Management Policy. If you assign permissions (a Policy) to a user group, all users in the group " +
"will have those permissions unless the user's permissions are higher than the group's permissions.",
CreateContext: resourceIdpGroupMappingCreate,
ReadContext: resourceIdpGroupMappingRead,
UpdateContext: resourceIdpGroupMappingUpdate,
DeleteContext: resourceIdpGroupMappingDelete,

Importer: &schema.ResourceImporter{
StateContext: schema.ImportStatePassthroughContext,
},

Schema: map[string]*schema.Schema{
"name": {
Type: schema.TypeString,
Description: "Name of the user group - should be unique in one account",
Required: true,
ForceNew: true,
ValidateDiagFunc: validations.DisallowEmptyString,
},
"policy": {
Type: schema.TypeList,
MinItems: 1,
Required: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"space_id": {
Type: schema.TypeString,
Description: "ID (slug) of the space the user group has access to",
Required: true,
ValidateDiagFunc: validations.DisallowEmptyString,
},
"role": {
Type: schema.TypeString,
Description: "Type of access to the space. Possible values are: " +
"READ, WRITE, ADMIN",
Required: true,
ValidateFunc: validation.StringInSlice(validAccessLevels, false),
},
},
},
},
},
}
}

func resourceIdpGroupMappingCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
// send a create query to the API
var mutation struct {
UserGroup *structs.UserGroup `graphql:"managedUserGroupCreate(input: $input)"`
}
variables := map[string]interface{}{
"input": structs.ManagedUserGroupCreateInput{
Name: toString(d.Get("name")),
AccessRules: getAccessRules(d),
},
}
if err := meta.(*internal.Client).Mutate(ctx, "ManagedUserGroupCreate", &mutation, variables); err != nil {
return diag.Errorf("could not create user group mapping %v: %v", toString(d.Get("name")), internal.FromSpaceliftError(err))
}

// set the ID in TF state
d.SetId(mutation.UserGroup.ID)

// fetch from remote and write to TF state
return resourceIdpGroupMappingRead(ctx, d, meta)
}

func resourceIdpGroupMappingRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
// send a read query to the API
var query struct {
UserGroup *structs.UserGroup `graphql:"managedUserGroup(id: $id)"`
}
variables := map[string]interface{}{"id": graphql.ID(d.Id())}
if err := meta.(*internal.Client).Query(ctx, "ManagedUserGroupRead", &query, variables); err != nil {
return diag.Errorf("could not query for user group mapping: %v", err)
}

// if the mapping is not found on the Spacelift side, delete it from the TF state
userGroup := query.UserGroup
if userGroup == nil {
d.SetId("")
return nil
}

// if found, update the TF state
d.Set("name", userGroup.Name)
var accessList []interface{}
for _, a := range userGroup.AccessRules {
accessList = append(accessList, map[string]interface{}{
"space_id": a.Space,
"role": a.SpaceAccessLevel,
})
}
d.Set("policy", accessList)

return nil

}

func resourceIdpGroupMappingUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
var ret diag.Diagnostics

// send an update query to the API
var mutation struct {
UserGroup *structs.UserGroup `graphql:"managedUserGroupUpdate(input: $input)"`
}
variables := map[string]interface{}{
"input": structs.ManagedUserGroupUpdateInput{
ID: toID(d.Id()),
AccessRules: getAccessRules(d),
},
}
if err := meta.(*internal.Client).Mutate(ctx, "ManagedUserGroupUpdate", &mutation, variables); err != nil {
ret = append(ret, diag.Errorf("could not update user group mapping: %v", internal.FromSpaceliftError(err))...)
}

// send a read query to the API
ret = append(ret, resourceIdpGroupMappingRead(ctx, d, meta)...)

return ret
}

func resourceIdpGroupMappingDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
// send a delete query to the API
var mutation struct {
UserGroup *structs.UserGroup `graphql:"managedUserGroupDelete(id: $id)"`
}
variables := map[string]interface{}{"id": toID(d.Id())}
if err := meta.(*internal.Client).Mutate(ctx, "ManagedUserGroupDelete", &mutation, variables); err != nil {
return diag.Errorf("could not delete user group mapping: %v", internal.FromSpaceliftError(err))
}

// if the mapping was successfully removed from the Spacelift side, delete it from the TF state
d.SetId("")

return nil
}

func getAccessRules(d *schema.ResourceData) []structs.SpaceAccessRuleInput {
var accessRules []structs.SpaceAccessRuleInput
for _, a := range d.Get("policy").([]interface{}) {
access := a.(map[string]interface{})
accessRules = append(accessRules, structs.SpaceAccessRuleInput{
Space: toID(access["space_id"]),
SpaceAccessLevel: structs.SpaceAccessLevel(access["role"].(string)),
})
}
return accessRules
}
97 changes: 97 additions & 0 deletions spacelift/resource_idp_group_mapping_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package spacelift

import (
"fmt"
"testing"

"github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"

. "github.com/spacelift-io/terraform-provider-spacelift/spacelift/internal/testhelpers"
)

var withOneAccess = `
resource "spacelift_idp_group_mapping" "test" {
name = "%s"
policy {
space_id = "root"
role = "ADMIN"
}
}
`

var withTwoAccesses = `
resource "spacelift_idp_group_mapping" "test" {
name = "%s"
policy {
space_id = "root"
role = "ADMIN"
}
policy {
space_id = "legacy"
role = "READ"
}
}
`

func TestIdpGroupMappingResource(t *testing.T) {
const resourceName = "spacelift_idp_group_mapping.test"

t.Run("creates and updates a user group mapping without an error", func(t *testing.T) {
oldName := "old name"
randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum)

testSteps(t, []resource.TestStep{
{
Config: fmt.Sprintf(withOneAccess, oldName),
Check: Resource(
resourceName,
Attribute("name", Equals(oldName)),
SetContains("policy", "root"),
SetContains("policy", "ADMIN"),
),
},
{
ResourceName: resourceName,
ImportState: true,
ImportStateVerify: true,
},
{
Config: fmt.Sprintf(withOneAccess, randomID),
Check: Resource(
resourceName,
Attribute("name", Equals(randomID)),
),
},
})
})

t.Run("can remove one access", func(t *testing.T) {
randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum)

testSteps(t, []resource.TestStep{
{
Config: fmt.Sprintf(withTwoAccesses, randomID),
Check: Resource(
resourceName,
Attribute("name", Equals(randomID)),
SetContains("policy", "root"),
SetContains("policy", "ADMIN"),
SetContains("policy", "legacy"),
SetContains("policy", "READ"),
),
},
{
Config: fmt.Sprintf(withOneAccess, randomID),
Check: Resource(
resourceName,
SetContains("policy", "root"),
SetContains("policy", "ADMIN"),
SetDoesNotContain("policy", "legacy"),
SetDoesNotContain("policy", "READ"),
),
},
})
})

}

0 comments on commit 1e0ed0c

Please sign in to comment.