Skip to content
Open
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
35 changes: 35 additions & 0 deletions docs/resources/security_user_roles.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
---
page_title: "Resource nexus_security_user_roles"
subcategory: "Security"
description: |-
Use this resource to manage roles of existing users.
Conflicts with nexussecurityuser, it will cause drifts
---
# Resource nexus_security_user_roles
Use this resource to manage roles of existing users.

Conflicts with nexus_security_user, it will cause drifts
## Example Usage
```terraform
resource "nexus_security_user_roles" "anonymous" {
userid = "anonymous"
roles = ["nx-anonymous", "example-role"]
}
```
<!-- schema generated by tfplugindocs -->
## Schema

### Required

- `roles` (Set of String) The roles which the user will be assigned within Nexus.
- `userid` (String) User ID of the user whose roles you wish to manage using Terraform. Must exist in Nexus.

### Read-Only

- `id` (String) Used to identify resource at nexus
## Import
Import is supported using the following syntax:
```shell
# import using the userid of the user
terraform import nexus_security_user_roles.anonymous anonymous
```
2 changes: 2 additions & 0 deletions examples/resources/nexus_security_user_roles/import.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# import using the userid of the user
terraform import nexus_security_user_roles.anonymous anonymous
4 changes: 4 additions & 0 deletions examples/resources/nexus_security_user_roles/resource.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
resource "nexus_security_user_roles" "anonymous" {
userid = "anonymous"
roles = ["nx-anonymous", "example-role"]
}
1 change: 1 addition & 0 deletions internal/provider/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ func Provider() *schema.Provider {
"nexus_security_saml": security.ResourceSecuritySAML(),
"nexus_security_user": security.ResourceSecurityUser(),
"nexus_security_user_token": security.ResourceSecurityUserToken(),
"nexus_security_user_roles": security.ResourceSecurityUserRoles(),
"nexus_user": deprecated.ResourceUser(),
"nexus_mail_config": other.ResourceMailConfig(),
},
Expand Down
216 changes: 216 additions & 0 deletions internal/services/security/resource_security_user_roles.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
package security

import (
"errors"
"fmt"
nexus "github.com/datadrivers/go-nexus-client/nexus3"
"github.com/datadrivers/terraform-provider-nexus/internal/schema/common"
"github.com/datadrivers/terraform-provider-nexus/internal/tools"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"log"
"sort"
)

func ResourceSecurityUserRoles() *schema.Resource {
return &schema.Resource{
Description: "Use this resource to manage roles of existing users.\n\n" +
"Conflicts with nexus_security_user, it will cause drifts",

Create: resourceSecurityUserRolesCreate,
Read: resourceSecurityUserRolesRead,
Update: resourceSecurityUserRolesUpdate,
Delete: resourceSecurityUserRolesDelete,
Exists: resourceSecurityUserRolesExists,
Importer: &schema.ResourceImporter{
StateContext: schema.ImportStatePassthroughContext,
},

Schema: map[string]*schema.Schema{
"id": common.ResourceID,
"userid": {
Description: "User ID of the user whose roles you wish to manage using Terraform. Must exist in Nexus.",
ForceNew: true,
Type: schema.TypeString,
Required: true,
},
"roles": {
Description: "The roles which the user will be assigned within Nexus.",
Elem: &schema.Schema{Type: schema.TypeString},
Type: schema.TypeSet,
Required: true,
},
},
}
}

// Function to remove duplicate elements from array
// https://codereview.stackexchange.com/a/192954
func resourceSecurityUserRolesUnique(slice []string) []string {
// create a map with all the values as key
uniqMap := make(map[string]struct{})
for _, v := range slice {
uniqMap[v] = struct{}{}
}

// turn the map keys into a slice
uniqSlice := make([]string, 0, len(uniqMap))
for v := range uniqMap {
uniqSlice = append(uniqSlice, v)
}
return uniqSlice
}

// Create a Terraform representation for an existing Nexus user with Terraform-managed roles
// Note: we have to load the whole Nexus user object, patch it and then write it back to Nexus as Nexus doesn't support
// PATCH, only PUT.
func resourceSecurityUserRolesCreate(d *schema.ResourceData, m interface{}) error {
log.Printf("[TRACE] resourceSecurityUserRolesCreate called")
client := m.(*nexus.NexusClient)

// First, try to load the user
userId := d.Get("userid").(string)
log.Printf("[TRACE] Calling User.Get(%s)", d.Id())
user, err := client.Security.User.Get(userId)
if err != nil {
log.Printf("[ERROR] User.Get(%s) failed", d.Id())
return err
}
if user == nil {
d.SetId("")
return errors.New(fmt.Sprintf("User.Get(%s) from Nexus is nil", userId))
}
log.Printf("[TRACE] Got user object:\n%+v\n", user)

// Now, set the Roles with the ones we specified - merging with existing roles in the process!
// Ideally, these should only be {"nx-anonymous"} (every user has at least this)
// Unfortunately, this *will* lead to drift if nx-anonymous or other roles the user has are not specified in the
// resource definition.
newRoles := tools.InterfaceSliceToStringSlice(d.Get("roles").(*schema.Set).List())
// Create a new slice that holds both the old and new roles as well as sorts and de-duplicates them
// as Nexus will throw an error otherwise if it encounters duplicates.
// See https://stackoverflow.com/a/58726780
finalRoles := make([]string, len(user.Roles), len(user.Roles)+len(newRoles))
copy(finalRoles, user.Roles)
finalRoles = append(finalRoles, newRoles...)
sort.Strings(finalRoles)
user.Roles = resourceSecurityUserRolesUnique(finalRoles)

// Finally, write the patched object to Nexus.
log.Printf("[TRACE] Writing user object:\n%+v\n", user)
if err := client.Security.User.Update(userId, *user); err != nil {
log.Printf("[ERROR] User.Update(%s) failed", userId)
return err
}
d.SetId(user.UserID)
return resourceSecurityUserRead(d, m)
}

// Update the state for a given Nexus user
func resourceSecurityUserRolesRead(d *schema.ResourceData, m interface{}) error {
log.Printf("[TRACE] resourceSecurityUserRolesRead called")
client := m.(*nexus.NexusClient)

userId := d.Id()
log.Printf("[TRACE] Calling User.Get(%s)", d.Id())
user, err := client.Security.User.Get(userId)
if err != nil {
log.Printf("[ERROR] User.Get(%s) failed", d.Id())
return err
}
if user == nil {
d.SetId("")
return errors.New(fmt.Sprintf("User.Get(%s) from Nexus is nil", userId))
}
log.Printf("[TRACE] Got user object:\n%+v\n", user)

err = d.Set("roles", tools.StringSliceToInterfaceSlice(user.Roles))
if err != nil {
log.Printf("[ERROR] d.Set(roles) failed")
return err
}
err = d.Set("userid", user.UserID)
if err != nil {
log.Printf("[ERROR] d.Set(userid) failed")
return err
}

return nil
}

// Update a Nexus user's roles if drift detection recognizes a change
func resourceSecurityUserRolesUpdate(d *schema.ResourceData, m interface{}) error {
log.Printf("[DEBUG] resourceSecurityUserRolesUpdate called")
client := m.(*nexus.NexusClient)

if d.HasChange("roles") {
userId := d.Id()
log.Printf("[TRACE] Calling User.Get(%s)", d.Id())
user, err := client.Security.User.Get(userId)
if err != nil {
log.Printf("[ERROR] User.Get(%s) failed", d.Id())
return err
}
if user == nil {
d.SetId("")
return errors.New(fmt.Sprintf("User.Get(%s) from Nexus is nil", userId))
}
log.Printf("[TRACE] Got user object:\n%+v\n", user)

finalRoles := tools.InterfaceSliceToStringSlice(d.Get("roles").(*schema.Set).List())
sort.Strings(finalRoles)
user.Roles = resourceSecurityUserRolesUnique(finalRoles)

log.Printf("[DEBUG] Writing user object:\n%+v\n", user)
if err := client.Security.User.Update(userId, *user); err != nil {
log.Printf("[ERROR] User.Update(%s) failed", userId)
return err
}
}
return resourceSecurityUserRolesRead(d, m)
}

// Delete all managed Nexus user roles
// Note: we do NOT delete the user object here because the user may originate from somewhere else (e.g. LDAP)
// Instead we remove all their roles but nx-anonymous
// TODO: store roles that an user had prior to creation in an attribute and restore that?
func resourceSecurityUserRolesDelete(d *schema.ResourceData, m interface{}) error {
log.Printf("[DEBUG] resourceSecurityUserRolesDelete called")
client := m.(*nexus.NexusClient)

// We only manage roles here. So, we simply set the user's roles to nx-anonymous
// as we can't pass an empty roles array (the Nexus API will error out
userId := d.Id()
log.Printf("[TRACE] Calling User.Get(%s)", d.Id())
user, err := client.Security.User.Get(userId)
if err != nil {
log.Printf("[ERROR] User.Get(%s) failed", d.Id())
return err
}
if user == nil {
d.SetId("")
return errors.New(fmt.Sprintf("User.Get(%s) from Nexus is nil", userId))
}
log.Printf("[TRACE] Got user object:\n%+v\n", user)

user.Roles = []string{"nx-anonymous"}

log.Printf("[DEBUG] Writing user object:\n%+v\n", user)
if err := client.Security.User.Update(userId, *user); err != nil {
log.Printf("[ERROR] User.Update(%s) failed", userId)
return err
}
d.SetId("")
return nil
}

// Check if Nexus has a user
func resourceSecurityUserRolesExists(d *schema.ResourceData, m interface{}) (bool, error) {
log.Printf("[DEBUG] resourceSecurityUserRolesExists called")
client := m.(*nexus.NexusClient)

user, err := client.Security.User.Get(d.Id())
if err != nil {
log.Printf("[ERROR] User.Get failed")
}
return user != nil, err
}
63 changes: 63 additions & 0 deletions internal/services/security/resource_security_user_roles_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package security_test

import (
"fmt"
"strconv"
"strings"
"testing"

"github.com/datadrivers/go-nexus-client/nexus3/schema/security"
"github.com/datadrivers/terraform-provider-nexus/internal/acceptance"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
)

func testAccResourceSecurityUserRoles() security.User {
return security.User{
UserID: fmt.Sprintf("user-test-%s", acctest.RandString(10)),
Roles: []string{"nx-admin"},
}
}

func TestAccResourceSecurityUserRoles(t *testing.T) {
resName := "nexus_security_user_roles.acceptance"

user := testAccResourceSecurityUserRoles()

resource.Test(t, resource.TestCase{
PreCheck: func() { acceptance.AccPreCheck(t) },
Providers: acceptance.TestAccProviders,
Steps: []resource.TestStep{
{
Config: testAccResourceSecurityUserRolesConfig(user),
Check: resource.ComposeTestCheckFunc(

resource.TestCheckResourceAttr(resName, "id", user.UserID),
resource.TestCheckResourceAttr(resName, "userid", user.UserID),
resource.TestCheckResourceAttr(resName, "roles.#", strconv.Itoa(len(user.Roles))),
// FIXME: (BUG) Incorrect roles state representation.
// For some reasons, 1st element in array is not stored as roles.0, but instead it's stored
// as roles.3360874991 where 3360874991 is a "random" number.
// This number changes from test run to test run.
// It may be a pointer to int instead of int itself, but it's not clear and requires additional research.
// resource.TestCheckResourceAttr(resName, "roles.3360874991", "nx-admin"),
),
},
{
ResourceName: resName,
ImportStateId: user.UserID,
ImportState: true,
ImportStateVerify: true,
},
},
})
}

func testAccResourceSecurityUserRolesConfig(user security.User) string {
return fmt.Sprintf(`
resource "nexus_security_user_roles" "acceptance" {
userid = "%s"
roles = ["%s"]
}
`, user.UserID, strings.Join(user.Roles, "\", \""))
}