Skip to content

Commit

Permalink
feat: add support for managing user mapping
Browse files Browse the repository at this point in the history
Signed-off-by: Michal Wasilewski <[email protected]>
  • Loading branch information
mwasilew2 committed Oct 10, 2023
1 parent ed2a21e commit 381fca9
Show file tree
Hide file tree
Showing 6 changed files with 129 additions and 22 deletions.
12 changes: 12 additions & 0 deletions examples/resources/spacelift_user_mapping/resource.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
resource "spacelift_user_mapping" "test" {
email = "[email protected]"
username = "johnk"
policy {
space_id = "root"
role = "ADMIN"
}
policy {
space_id = "legacy"
role = "READ"
}
}
1 change: 1 addition & 0 deletions spacelift/internal/structs/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package structs

type User struct {
ID string `graphql:"id"`
Email string `graphql:"email"`
Username string `graphql:"username"`
AccessRules []SpaceAccessRule `graphql:"accessRules"`
}
9 changes: 7 additions & 2 deletions spacelift/internal/structs/user_input.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@ package structs

import "github.com/shurcooL/graphql"

type ManagedUserUpdateInput struct {
ID graphql.ID `json:"id"`
type UserInviteInput struct {
Email graphql.String `json:"email"`
Username graphql.String `json:"username"`
AccessRules []SpaceAccessRuleInput `json:"accessRules"`
}

type UserUpdateInput struct {
AccessRules []SpaceAccessRuleInput `json:"accessRules"`
}
2 changes: 1 addition & 1 deletion spacelift/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,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_user": resourceUser(),
"spacelift_user_mapping": resourceUserMapping(),
"spacelift_vcs_agent_pool": resourceVCSAgentPool(),
"spacelift_webhook": resourceWebhook(),
"spacelift_named_webhook": resourceNamedWebhook(),
Expand Down
60 changes: 41 additions & 19 deletions spacelift/resource_user.go → spacelift/resource_user_mapping.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,27 +12,35 @@ import (
"github.com/spacelift-io/terraform-provider-spacelift/spacelift/internal/validations"
)

func resourceUser() *schema.Resource {
func resourceUserMapping() *schema.Resource {
return &schema.Resource{
Description: "" +
"`spacelift_user` represents a Spacelift user. ",
CreateContext: resourceUserCreate,
ReadContext: resourceUserRead,
UpdateContext: resourceUserUpdate,
DeleteContext: resourceUserDelete,
"`spacelift_user_mapping` represents a mapping between a Spacelift user " +
"(managed using an Identity Provider) and a Policy. A Policy defines " +
"what access rights the user has to a given Space.",
CreateContext: resourceUserMappingCreate,
ReadContext: resourceUserMappingRead,
UpdateContext: resourceUserMappingUpdate,
DeleteContext: resourceUserMappingDelete,

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

Schema: map[string]*schema.Schema{
"email": {
Type: schema.TypeString,
Description: "Email of the user. Used for sending an invitation.",
Required: true,
},
"username": {
Type: schema.TypeString,
Description: "Username of the user",
Required: true,
},
"policy": {
Type: schema.TypeList,
Type: schema.TypeList,
Optional: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"space_id": {
Expand All @@ -55,15 +63,30 @@ func resourceUser() *schema.Resource {
}
}

func resourceUserCreate(ctx context.Context, d *schema.ResourceData, i interface{}) diag.Diagnostics {
// send a create query to the API
func resourceUserMappingCreate(ctx context.Context, d *schema.ResourceData, i interface{}) diag.Diagnostics {
// send an Invite (create) mutation to the API
var mutation struct {
User *structs.User `graphql:`
User *structs.User `graphql:"managedUserInvite(input: $iinput)"`
}
variables := map[string]interface{}{
"input": structs.UserInviteInput{
Email: toString(d.Get("email")),
Username: toString(d.Get("username")),
AccessRules: getAccessRules(d),
},
}
if err := i.(*internal.Client).Mutate(ctx, "ManagedUserInvite", &mutation, variables); err != nil {
return diag.Errorf("could not create user %s: %v", toString(d.Get("username")), internal.FromSpaceliftError(err))
}

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

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

func resourceUserRead(ctx context.Context, d *schema.ResourceData, i interface{}) diag.Diagnostics {
func resourceUserMappingRead(ctx context.Context, d *schema.ResourceData, i interface{}) diag.Diagnostics {
// send a read query to the API
var query struct {
User *structs.User `graphql:"managedUser(id: $id)"`
Expand All @@ -73,13 +96,14 @@ func resourceUserRead(ctx context.Context, d *schema.ResourceData, i interface{}
return diag.Errorf("could not query for user: %v", err)
}

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

// if found, update the TF state
d.Set("email", query.User.Email)
d.Set("username", query.User.Username)
var accessList []interface{}
for _, a := range query.User.AccessRules {
Expand All @@ -93,31 +117,29 @@ func resourceUserRead(ctx context.Context, d *schema.ResourceData, i interface{}
return nil
}

func resourceUserUpdate(ctx context.Context, d *schema.ResourceData, i interface{}) diag.Diagnostics {
func resourceUserMappingUpdate(ctx context.Context, d *schema.ResourceData, i interface{}) diag.Diagnostics {
var ret diag.Diagnostics

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

// send a read query to the API
ret = append(ret, resourceUserRead(ctx, d, i)...)
// fetch from remote and write to TF state
ret = append(ret, resourceUserMappingCreate(ctx, d, i)...)

return ret

}

func resourceUserDelete(ctx context.Context, d *schema.ResourceData, i interface{}) diag.Diagnostics {
func resourceUserMappingDelete(ctx context.Context, d *schema.ResourceData, i interface{}) diag.Diagnostics {
// send a delete query to the API
var mutation struct {
User *structs.User `graphql:"managedUserDelete(id: $id)"`
Expand Down
67 changes: 67 additions & 0 deletions spacelift/resource_user_mapping_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
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 userWithOneAccess = `
resource "spacelift_user_mapping" "test" {
email = "%s"
username = "%s"
policy {
space_id = "root"
role = "ADMIN"
}
}
`

func TestUserResource(t *testing.T) {
const resourceName = "spacelift_user_mapping.test"

t.Run("creates and updates a user mapping without an error", func(t *testing.T) {
randomEmail := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum)
randomUsername := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum)

newEmail := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum)
newUsername := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum)

testSteps(t, []resource.TestStep{
{
Config: fmt.Sprintf(userWithOneAccess, randomEmail, randomUsername),
Check: Resource(
resourceName,
Attribute("email", Equals(randomEmail)),
Attribute("username", Equals(randomUsername)),
SetContains("policy", "root"),
SetContains("policy", "ADMIN"),
),
},
{
ResourceName: resourceName,
ImportState: true,
ImportStateVerify: true,
},
{
Config: fmt.Sprintf(userWithOneAccess, newEmail, newUsername),
Check: Resource(
resourceName,
Attribute("email", Equals(newEmail)),
Attribute("username", Equals(newUsername)),
SetContains("policy", "root"),
SetContains("policy", "ADMIN"),
),
},
})
})

t.Run("can remove one access", func(t *testing.T) {

})

}

0 comments on commit 381fca9

Please sign in to comment.