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
6 changes: 5 additions & 1 deletion docs/resources/security_user.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,15 @@ resource "nexus_security_user" "admin" {
- `email` (String) The email address associated with the user.
- `firstname` (String) The first name of the user.
- `lastname` (String) The last name of the user.
- `password` (String, Sensitive) The password for the user.
- `userid` (String) The userid which is required for login. This value cannot be changed.

### Optional

> **NOTE**: [Write-only arguments](https://developer.hashicorp.com/terraform/language/resources/ephemeral#write-only-arguments) are supported in Terraform 1.11 and later.

- `password` (String, Sensitive) The password for the user.
- `password_wo` (String, Sensitive, [Write-only](https://developer.hashicorp.com/terraform/language/resources/ephemeral#write-only-arguments)) The password for the user (write-only, not stored in state). Use with password_wo_version to control updates.
- `password_wo_version` (Number) Version tracker for password_wo changes. Increment this value to force a password update when using password_wo.
- `roles` (Set of String) The roles which the user has been assigned within Nexus.
- `status` (String) The user's status, e.g. active or disabled.

Expand Down
26 changes: 26 additions & 0 deletions examples/resources/nexus_security_user/resource.tf
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,29 @@ resource "nexus_security_user" "admin" {
roles = ["nx-admin"]
status = "active"
}

resource "nexus_security_user" "user_password_wo" {
userid = "user_password_wo"
firstname = "Administrator"
lastname = "User"
email = "[email protected]"
password_wo = "admin123" # This password value don't save to state
password_wo_version = 1 # Incriment version, for update password
roles = ["nx-admin"]
status = "active"
}

ephemeral "random_password" "password" {
length = 16
}

resource "nexus_security_user" "user_password_from_ephemeral" {
userid = "user_ephemeral_password"
firstname = "ephemeral"
lastname = "User"
email = "[email protected]"
password_wo = ephemeral.random_password.password # Use ephemeral value
password_wo_version = 1 # Incriment version, for update password
roles = ["nx-admin"]
status = "active"
}
91 changes: 84 additions & 7 deletions internal/services/security/resource_security_user.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
package security

import (
"errors"
"fmt"

nexus "github.com/datadrivers/go-nexus-client/nexus3"
"github.com/datadrivers/go-nexus-client/nexus3/schema/security"
"github.com/datadrivers/terraform-provider-nexus/internal/schema/common"
"github.com/datadrivers/terraform-provider-nexus/internal/tools"
"github.com/hashicorp/go-cty/cty"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
)
Expand Down Expand Up @@ -46,10 +50,27 @@ func ResourceSecurityUser() *schema.Resource {
Required: true,
},
"password": {
Description: "The password for the user.",
Type: schema.TypeString,
Required: true,
Sensitive: true,
Description: "The password for the user.",
Type: schema.TypeString,
Optional: true,
Sensitive: true,
ConflictsWith: []string{"password_wo", "password_wo_version"},
},
"password_wo": {
Description: "The password for the user (write-only, not stored in state). Use with password_wo_version to control updates.",
Type: schema.TypeString,
Optional: true,
Sensitive: true,
WriteOnly: true,
ConflictsWith: []string{"password"},
RequiredWith: []string{"password_wo_version"},
},
"password_wo_version": {
Description: "Version tracker for password_wo changes. Increment this value to force a password update when using password_wo.",
Type: schema.TypeInt,
Optional: true,
ConflictsWith: []string{"password"},
RequiredWith: []string{"password_wo"},
},
"roles": {
Description: "The roles which the user has been assigned within Nexus.",
Expand Down Expand Up @@ -77,16 +98,41 @@ func getSecurityUserFromResourceData(d *schema.ResourceData) security.User {
FirstName: d.Get("firstname").(string),
LastName: d.Get("lastname").(string),
EmailAddress: d.Get("email").(string),
Password: d.Get("password").(string),
Password: getPasswordFromResourceData(d),
Status: d.Get("status").(string),
Roles: tools.InterfaceSliceToStringSlice(d.Get("roles").(*schema.Set).List()),
}
}

func getPasswordFromResourceData(d *schema.ResourceData) string {
if password := d.Get("password").(string); password != "" {
return password
}

if d.Get("password_wo_version").(int) != 0 && d.HasChange("password_wo_version") {
passwordWriteOnly, diags := d.GetRawConfigAt(cty.GetAttrPath("password_wo"))
if diags.HasError() {
return ""
}

if passwordWriteOnly.IsNull() || !passwordWriteOnly.IsKnown() {
return ""
}

return passwordWriteOnly.AsString()
}

return ""
}

func resourceSecurityUserCreate(d *schema.ResourceData, m interface{}) error {
client := m.(*nexus.NexusClient)
user := getSecurityUserFromResourceData(d)

if user.Password == "" {
return fmt.Errorf("either 'password' or 'password_wo' with 'password_wo_version' must be provided")
}

if err := client.Security.User.Create(user); err != nil {
return err
}
Expand Down Expand Up @@ -115,25 +161,56 @@ func resourceSecurityUserRead(d *schema.ResourceData, m interface{}) error {
d.Set("status", user.Status)
d.Set("userid", user.UserID)

if v, ok := d.GetOk("password_wo_version"); ok && v != nil {
d.Set("password_wo_version", v.(int))
} else {
if existingPassword, hasPassword := d.GetOk("password"); hasPassword {
d.Set("password", existingPassword.(string))
}
}

return nil
}

func resourceSecurityUserUpdate(d *schema.ResourceData, m interface{}) error {
client := m.(*nexus.NexusClient)

passwordChanged := false
var newPassword string

if d.HasChange("password") {
password := d.Get("password").(string)
if err := client.Security.User.ChangePassword(d.Id(), password); err != nil {
newPassword = d.Get("password").(string)
passwordChanged = true
}

if d.HasChange("password_wo_version") {
passwordWriteOnly, diags := d.GetRawConfigAt(cty.GetAttrPath("password_wo"))
if diags.HasError() {
return errors.New("error reading 'password_wo' argument")
}

if !passwordWriteOnly.IsNull() && passwordWriteOnly.IsKnown() {
newPassword = passwordWriteOnly.AsString()
passwordChanged = true
}
}

if passwordChanged && newPassword != "" {
if err := client.Security.User.ChangePassword(d.Id(), newPassword); err != nil {
return err
}
}

if d.HasChange("firstname") || d.HasChange("lastname") || d.HasChange("email") || d.HasChange("status") || d.HasChange("roles") {
user := getSecurityUserFromResourceData(d)
if !passwordChanged {
user.Password = ""
}
if err := client.Security.User.Update(d.Id(), user); err != nil {
return err
}
}

return resourceSecurityUserRead(d, m)
}

Expand Down
175 changes: 175 additions & 0 deletions internal/services/security/resource_security_user_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package security_test

import (
"fmt"
"regexp"
"strconv"
"strings"
"testing"
Expand Down Expand Up @@ -65,6 +66,100 @@ func TestAccResourceSecurityUser(t *testing.T) {
})
}

// Test write-only password functionality
func TestAccResourceSecurityUser_WriteOnlyPassword(t *testing.T) {
resName := "nexus_security_user.acceptance"

user := testAccResourceSecurityUser()
updatedFirstName := fmt.Sprintf("user-firstname-updated-%s", acctest.RandString(10))
newPassword := acctest.RandString(16)

resource.Test(t, resource.TestCase{
PreCheck: func() { acceptance.AccPreCheck(t) },
Providers: acceptance.TestAccProviders,
Steps: []resource.TestStep{
// Create user with write-only password
{
Config: testAccResourceSecurityUserWriteOnlyConfig(user, 1),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(resName, "id", user.UserID),
resource.TestCheckResourceAttr(resName, "userid", user.UserID),
resource.TestCheckResourceAttr(resName, "firstname", user.FirstName),
resource.TestCheckResourceAttr(resName, "lastname", user.LastName),
resource.TestCheckResourceAttr(resName, "email", user.EmailAddress),
resource.TestCheckResourceAttr(resName, "status", user.Status),
resource.TestCheckResourceAttr(resName, "password_wo_version", "1"),
resource.TestCheckResourceAttr(resName, "roles.#", strconv.Itoa(len(user.Roles))),
// password_wo should NOT be in state
resource.TestCheckNoResourceAttr(resName, "password_wo"),
// legacy password should NOT be set
resource.TestCheckNoResourceAttr(resName, "password"),
),
},
// Update non-password fields (should not trigger password change)
{
Config: testAccResourceSecurityUserWriteOnlyConfigWithUpdatedName(user, updatedFirstName, 1),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(resName, "firstname", updatedFirstName),
resource.TestCheckResourceAttr(resName, "password_wo_version", "1"), // Version unchanged
),
},
// Update password by changing version
{
Config: testAccResourceSecurityUserWriteOnlyConfigWithPasswordUpdate(user, updatedFirstName, newPassword, 2),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(resName, "firstname", updatedFirstName),
resource.TestCheckResourceAttr(resName, "password_wo_version", "2"), // Version incremented
// password_wo should still NOT be in state
resource.TestCheckNoResourceAttr(resName, "password_wo"),
),
},
// Import test for write-only password
{
ResourceName: resName,
ImportStateId: user.UserID,
ImportState: true,
ImportStateVerify: true,
// password_wo is write-only and should be ignored
ImportStateVerifyIgnore: []string{"password_wo"},
},
},
})
}

// Test that legacy and write-only password fields conflict
func TestAccResourceSecurityUser_PasswordConflict(t *testing.T) {
user := testAccResourceSecurityUser()

resource.Test(t, resource.TestCase{
PreCheck: func() { acceptance.AccPreCheck(t) },
Providers: acceptance.TestAccProviders,
Steps: []resource.TestStep{
{
Config: testAccResourceSecurityUserConflictConfig(user),
ExpectError: regexp.MustCompile("conflicts with password"),
},
},
})
}

// Test that password_wo requires password_wo_version
func TestAccResourceSecurityUser_WriteOnlyPasswordRequiresVersion(t *testing.T) {
user := testAccResourceSecurityUser()

resource.Test(t, resource.TestCase{
PreCheck: func() { acceptance.AccPreCheck(t) },
Providers: acceptance.TestAccProviders,
Steps: []resource.TestStep{
{
Config: testAccResourceSecurityUserWriteOnlyNoVersionConfig(user),
ExpectError: regexp.MustCompile("all of `password_wo,password_wo_version` must be specified"), // ← Исправленный regex
},
},
})
}

// Legacy password configuration (backward compatibility)
func testAccResourceSecurityUserConfig(user security.User) string {
return fmt.Sprintf(`
resource "nexus_security_user" "acceptance" {
Expand All @@ -78,3 +173,83 @@ resource "nexus_security_user" "acceptance" {
}
`, user.UserID, user.FirstName, user.LastName, user.EmailAddress, user.Password, user.Status, strings.Join(user.Roles, "\", \""))
}

// Write-only password configuration
func testAccResourceSecurityUserWriteOnlyConfig(user security.User, version int) string {
return fmt.Sprintf(`
resource "nexus_security_user" "acceptance" {
userid = "%s"
firstname = "%s"
lastname = "%s"
email = "%s"
password_wo = "%s"
password_wo_version = %d
status = "%s"
roles = ["%s"]
}
`, user.UserID, user.FirstName, user.LastName, user.EmailAddress, user.Password, version, user.Status, strings.Join(user.Roles, "\", \""))
}

// Write-only password configuration with updated firstname
func testAccResourceSecurityUserWriteOnlyConfigWithUpdatedName(user security.User, updatedFirstName string, version int) string {
return fmt.Sprintf(`
resource "nexus_security_user" "acceptance" {
userid = "%s"
firstname = "%s"
lastname = "%s"
email = "%s"
password_wo = "%s"
password_wo_version = %d
status = "%s"
roles = ["%s"]
}
`, user.UserID, updatedFirstName, user.LastName, user.EmailAddress, user.Password, version, user.Status, strings.Join(user.Roles, "\", \""))
}

// Write-only password configuration with password update
func testAccResourceSecurityUserWriteOnlyConfigWithPasswordUpdate(user security.User, firstName, newPassword string, version int) string {
return fmt.Sprintf(`
resource "nexus_security_user" "acceptance" {
userid = "%s"
firstname = "%s"
lastname = "%s"
email = "%s"
password_wo = "%s"
password_wo_version = %d
status = "%s"
roles = ["%s"]
}
`, user.UserID, firstName, user.LastName, user.EmailAddress, newPassword, version, user.Status, strings.Join(user.Roles, "\", \""))
}

// Configuration that should cause conflict error
func testAccResourceSecurityUserConflictConfig(user security.User) string {
return fmt.Sprintf(`
resource "nexus_security_user" "acceptance" {
userid = "%s"
firstname = "%s"
lastname = "%s"
email = "%s"
password = "%s"
password_wo = "%s"
password_wo_version = 1
status = "%s"
roles = ["%s"]
}
`, user.UserID, user.FirstName, user.LastName, user.EmailAddress, user.Password, user.Password, user.Status, strings.Join(user.Roles, "\", \""))
}

// Configuration with password_wo but no version (should fail)
func testAccResourceSecurityUserWriteOnlyNoVersionConfig(user security.User) string {
return fmt.Sprintf(`
resource "nexus_security_user" "acceptance" {
userid = "%s"
firstname = "%s"
lastname = "%s"
email = "%s"
password_wo = "%s"
status = "%s"
roles = ["%s"]
}
`, user.UserID, user.FirstName, user.LastName, user.EmailAddress, user.Password, user.Status, strings.Join(user.Roles, "\", \""))
}