diff --git a/docs/content/resources/user.group.md b/docs/content/resources/user.group.md index 5839312e9..bc763f058 100644 --- a/docs/content/resources/user.group.md +++ b/docs/content/resources/user.group.md @@ -1,7 +1,7 @@ --- title: "user.group" slug: "user-group" -date: "2016-10-04T13:01:50-05:00" +date: "2016-10-28T08:47:08-05:00" menu: main: parent: resources @@ -43,5 +43,6 @@ The group Name will be changed to NewName. Valid values: `present` and `absent` State is whether the group should be present. +The default value is present. diff --git a/docs/content/resources/user.user.md b/docs/content/resources/user.user.md index 78afcd9fe..db186ebfb 100644 --- a/docs/content/resources/user.user.md +++ b/docs/content/resources/user.user.md @@ -1,7 +1,7 @@ --- title: "user.user" slug: "user-user" -date: "2016-10-04T13:01:50-05:00" +date: "2016-10-28T08:47:08-05:00" menu: main: parent: resources @@ -28,6 +28,13 @@ user.user "user" { Username is the user login name. +- `new_username` (string) + + NewUsername is used when modifying a user. +Username will be changed to NewUsername. No changes to the home directory +name or location of the contents will be made. This can be done using +HomeDir and MoveDir options. + - `uid` (optional uint32) UID is the user ID. @@ -51,11 +58,32 @@ Only one of GID or Groupname may be indicated. - `name` (string) Name is the user description. +This field can be indicated when adding or modifying a user. + +- `create_home` (bool) + + CreateHome when set to true will create the home directory for the user. +The files and directories contained in the skeleton directory (which can be +defined with the SkelDir option) will be copied to the home directory. + +- `skel_dir` (string) + + SkelDir contains files and directories to be copied in the user's home +directory when adding a user. If not set, the skeleton directory is defined +by the SKEL variable in /etc/default/useradd or, by default, /etc/skel. +SkelDir is only valid is CreatHome is specified. - `home_dir` (string) - HomeDir is the user's login directory. By default, the login -name is appended to the home directory. + HomeDir is the name of the user's login directory. If not set, the home +directory is defined by appending the value of Username to the HOME +variable in /etc/default/useradd, resulting in /HOME/Username. +This field can be indicated when adding or modifying a user. + +- `move_dir` (bool) + + MoveDir is used to move the contents of HomeDir when modifying a user. +HomeDir must also be indicated if MoveDir is set to true. - `state` (State) @@ -63,5 +91,6 @@ name is appended to the home directory. Valid values: `present` and `absent` State is whether the user should be present. +The default value is present. diff --git a/resource/group/preparer.go b/resource/group/preparer.go index fc2c45c2e..7d54e327b 100644 --- a/resource/group/preparer.go +++ b/resource/group/preparer.go @@ -37,6 +37,7 @@ type Preparer struct { NewName string `hcl:"new_name"` // State is whether the group should be present. + // The default value is present. State State `hcl:"state" valid_values:"present,absent"` } diff --git a/resource/user/preparer.go b/resource/user/preparer.go index d0cac1d53..2ae4bcced 100644 --- a/resource/user/preparer.go +++ b/resource/user/preparer.go @@ -29,6 +29,12 @@ type Preparer struct { // Username is the user login name. Username string `hcl:"username" required:"true"` + // NewUsername is used when modifying a user. + // Username will be changed to NewUsername. No changes to the home directory + // name or location of the contents will be made. This can be done using + // HomeDir and MoveDir options. + NewUsername string `hcl:"new_username"` + // UID is the user ID. UID *uint32 `hcl:"uid"` @@ -41,13 +47,32 @@ type Preparer struct { GID *uint32 `hcl:"gid" mutually_exclusive:"gid,groupname"` // Name is the user description. + // This field can be indicated when adding or modifying a user. Name string `hcl:"name"` - // HomeDir is the user's login directory. By default, the login - // name is appended to the home directory. + // CreateHome when set to true will create the home directory for the user. + // The files and directories contained in the skeleton directory (which can be + // defined with the SkelDir option) will be copied to the home directory. + CreateHome bool `hcl:"create_home"` + + // SkelDir contains files and directories to be copied in the user's home + // directory when adding a user. If not set, the skeleton directory is defined + // by the SKEL variable in /etc/default/useradd or, by default, /etc/skel. + // SkelDir is only valid is CreatHome is specified. + SkelDir string `hcl:"skel_dir"` + + // HomeDir is the name of the user's login directory. If not set, the home + // directory is defined by appending the value of Username to the HOME + // variable in /etc/default/useradd, resulting in /HOME/Username. + // This field can be indicated when adding or modifying a user. HomeDir string `hcl:"home_dir"` + // MoveDir is used to move the contents of HomeDir when modifying a user. + // HomeDir must also be indicated if MoveDir is set to true. + MoveDir bool `hcl:"move_dir"` + // State is whether the user should be present. + // The default value is present. State State `hcl:"state" valid_values:"present,absent"` } @@ -63,15 +88,27 @@ func (p *Preparer) Prepare(render resource.Renderer) (resource.Task, error) { return nil, fmt.Errorf("user \"gid\" parameter out of range") } + if p.SkelDir != "" && !p.CreateHome { + return nil, fmt.Errorf("user \"create_home\" parameter required with \"skel_dir\" parameter") + } + + if p.MoveDir && p.HomeDir == "" { + return nil, fmt.Errorf("user \"home_dir\" parameter required with \"move_dir\" parameter") + } + if p.State == "" { p.State = StatePresent } usr := NewUser(new(System)) usr.Username = p.Username + usr.NewUsername = p.NewUsername usr.GroupName = p.GroupName usr.Name = p.Name + usr.CreateHome = p.CreateHome + usr.SkelDir = p.SkelDir usr.HomeDir = p.HomeDir + usr.MoveDir = p.MoveDir usr.State = p.State if p.UID != nil { diff --git a/resource/user/preparer_test.go b/resource/user/preparer_test.go index eb5cd113d..b279943da 100644 --- a/resource/user/preparer_test.go +++ b/resource/user/preparer_test.go @@ -99,6 +99,20 @@ func TestPrepare(t *testing.T) { assert.NoError(t, err) }) + t.Run("create_home and skel_dir parameters", func(t *testing.T) { + p := user.Preparer{UID: &testID, GID: &testID, Username: "test", CreateHome: true, SkelDir: "/etc/skel", HomeDir: "tmp", State: user.StateAbsent} + _, err := p.Prepare(&fr) + + assert.NoError(t, err) + }) + + t.Run("create_home parameter", func(t *testing.T) { + p := user.Preparer{UID: &testID, GID: &testID, Username: "test", CreateHome: true, HomeDir: "tmp", State: user.StateAbsent} + _, err := p.Prepare(&fr) + + assert.NoError(t, err) + }) + t.Run("no name parameter", func(t *testing.T) { p := user.Preparer{UID: &testID, GID: &testID, Username: "test", HomeDir: "tmp", State: user.StateAbsent} _, err := p.Prepare(&fr) @@ -119,6 +133,13 @@ func TestPrepare(t *testing.T) { assert.NoError(t, err) }) + + t.Run("home_dir and move_dir parameters", func(t *testing.T) { + p := user.Preparer{Username: "test", MoveDir: true, HomeDir: "tmp"} + _, err := p.Prepare(&fr) + + assert.NoError(t, err) + }) }) t.Run("invalid", func(t *testing.T) { @@ -135,5 +156,19 @@ func TestPrepare(t *testing.T) { assert.EqualError(t, err, fmt.Sprintf("user \"gid\" parameter out of range")) }) + + t.Run("no create_home with skel_dir", func(t *testing.T) { + p := user.Preparer{UID: &testID, GID: &testID, Username: "test", SkelDir: "/etc/skel", HomeDir: "tmp", State: user.StateAbsent} + _, err := p.Prepare(&fr) + + assert.EqualError(t, err, fmt.Sprintf("user \"create_home\" parameter required with \"skel_dir\" parameter")) + }) + + t.Run("no home_dir with move_dir", func(t *testing.T) { + p := user.Preparer{Username: "test", MoveDir: true} + _, err := p.Prepare(&fr) + + assert.EqualError(t, err, fmt.Sprintf("user \"home_dir\" parameter required with \"move_dir\" parameter")) + }) }) } diff --git a/resource/user/user.go b/resource/user/user.go index 525ca9248..867d24662 100644 --- a/resource/user/user.go +++ b/resource/user/user.go @@ -35,29 +35,49 @@ const ( // User manages user users type User struct { - Username string - UID string - GroupName string - GID string - Name string - HomeDir string - State State - system SystemUtils + Username string + NewUsername string + UID string + GroupName string + GID string + Name string + CreateHome bool + SkelDir string + HomeDir string + MoveDir bool + State State + system SystemUtils + + *resource.Status } // AddUserOptions are the options specified in the configuration to be used // when adding a user type AddUserOptions struct { + UID string + Group string + Comment string + CreateHome bool + SkelDir string + Directory string +} + +// ModUserOptions are the options specified in the configuration to be used +// when modifying a user +type ModUserOptions struct { + Username string UID string Group string Comment string Directory string + MoveDir bool } // SystemUtils provides system utilities for user type SystemUtils interface { AddUser(userName string, options *AddUserOptions) error DelUser(userName string) error + ModUser(userName string, options *ModUserOptions) error Lookup(userName string) (*user.User, error) LookupID(userID string) (*user.User, error) LookupGroup(groupName string) (*user.Group, error) @@ -90,57 +110,33 @@ func (u *User) Check(resource.Renderer) (resource.TaskStatus, error) { userByID, uidErr = u.system.LookupID(u.UID) } - status := &resource.Status{} + u.Status = resource.NewStatus() if nameErr == ErrUnsupported { - status.RaiseLevel(resource.StatusFatal) - return status, ErrUnsupported + u.Status.RaiseLevel(resource.StatusFatal) + return u, ErrUnsupported } switch u.State { case StatePresent: - switch { - case u.UID == "": - _, nameNotFound := nameErr.(user.UnknownUserError) + _, nameNotFound := nameErr.(user.UnknownUserError) - switch { - case userByName != nil: - status.AddMessage(fmt.Sprintf("user %s already exists", u.Username)) - case nameNotFound: - _, err := SetAddUserOptions(u) - if err != nil { - status.RaiseLevel(resource.StatusCantChange) - return status, errors.Wrapf(err, "cannot add user %s", u.Username) - } - status.RaiseLevel(resource.StatusWillChange) - status.AddMessage("user does not exist") - status.AddDifference("user", string(StateAbsent), fmt.Sprintf("user %s", u.Username), "") + switch { + case nameNotFound: + _, err := u.DiffAdd(u.Status) + if err != nil { + return u, errors.Wrapf(err, "cannot add user %s", u.Username) } - case u.UID != "": - _, nameNotFound := nameErr.(user.UnknownUserError) - _, uidNotFound := uidErr.(user.UnknownUserIdError) - - switch { - case nameNotFound && uidNotFound: - _, err := SetAddUserOptions(u) - if err != nil { - status.RaiseLevel(resource.StatusCantChange) - return status, errors.Wrapf(err, "cannot add user %s with uid %s", u.Username, u.UID) - } - status.RaiseLevel(resource.StatusWillChange) - status.AddMessage("user name and uid do not exist") - status.AddDifference("user", string(StateAbsent), fmt.Sprintf("user %s with uid %s", u.Username, u.UID), "") - case nameNotFound: - status.RaiseLevel(resource.StatusCantChange) - return status, fmt.Errorf("cannot add user %s with uid %s: user uid already exists", u.Username, u.UID) - case uidNotFound: - status.RaiseLevel(resource.StatusCantChange) - return status, fmt.Errorf("cannot add user %s with uid %s: user already exists", u.Username, u.UID) - case userByName != nil && userByID != nil && userByName.Name != userByID.Name || userByName.Uid != userByID.Uid: - status.RaiseLevel(resource.StatusCantChange) - return status, fmt.Errorf("cannot add user %s with uid %s: user and uid belong to different users", u.Username, u.UID) - case userByName != nil && userByID != nil && *userByName == *userByID: - status.AddMessage("user %s with uid %s already exists", u.Username, u.UID) + if resource.AnyChanges(u.Status.Differences) { + u.Status.AddMessage("add user") + } + case userByName != nil: + _, err := u.DiffMod(u.Status, userByName) + if err != nil { + return u, errors.Wrapf(err, "cannot modify user %s", u.Username) + } + if resource.AnyChanges(u.Status.Differences) { + u.Status.AddMessage("modify user") } } case StateAbsent: @@ -150,10 +146,10 @@ func (u *User) Check(resource.Renderer) (resource.TaskStatus, error) { switch { case nameNotFound: - status.AddMessage(fmt.Sprintf("user %s does not exist", u.Username)) + u.Status.AddMessage(fmt.Sprintf("user %s does not exist", u.Username)) case userByName != nil: - status.RaiseLevel(resource.StatusWillChange) - status.AddDifference("user", fmt.Sprintf("user %s", u.Username), string(StateAbsent), "") + u.Status.RaiseLevel(resource.StatusWillChange) + u.Status.AddDifference("user", fmt.Sprintf("user %s", u.Username), fmt.Sprintf("<%s>", string(StateAbsent)), "") } case u.UID != "": _, nameNotFound := nameErr.(user.UnknownUserError) @@ -161,27 +157,27 @@ func (u *User) Check(resource.Renderer) (resource.TaskStatus, error) { switch { case nameNotFound && uidNotFound: - status.AddMessage(fmt.Sprintf("user %s and uid %s do not exist", u.Username, u.UID)) + u.Status.AddMessage(fmt.Sprintf("user %s and uid %s do not exist", u.Username, u.UID)) case nameNotFound: - status.RaiseLevel(resource.StatusCantChange) - return status, fmt.Errorf("cannot delete user %s with uid %s: user does not exist", u.Username, u.UID) + u.Status.RaiseLevel(resource.StatusCantChange) + return u, fmt.Errorf("cannot delete user %s with uid %s: user does not exist", u.Username, u.UID) case uidNotFound: - status.RaiseLevel(resource.StatusCantChange) - return status, fmt.Errorf("cannot delete user %s with uid %s: uid does not exist", u.Username, u.UID) + u.Status.RaiseLevel(resource.StatusCantChange) + return u, fmt.Errorf("cannot delete user %s with uid %s: uid does not exist", u.Username, u.UID) case userByName != nil && userByID != nil && userByName.Name != userByID.Name || userByName.Uid != userByID.Uid: - status.RaiseLevel(resource.StatusCantChange) - return status, fmt.Errorf("cannot delete user %s with uid %s: user and uid belong to different users", u.Username, u.UID) + u.Status.RaiseLevel(resource.StatusCantChange) + return u, fmt.Errorf("cannot delete user %s with uid %s: user and uid belong to different users", u.Username, u.UID) case userByName != nil && userByID != nil && *userByName == *userByID: - status.RaiseLevel(resource.StatusWillChange) - status.AddDifference("user", fmt.Sprintf("user %s with uid %s", u.Username, u.UID), string(StateAbsent), "") + u.Status.RaiseLevel(resource.StatusWillChange) + u.Status.AddDifference("user", fmt.Sprintf("user %s with uid %s", u.Username, u.UID), fmt.Sprintf("<%s>", string(StateAbsent)), "") } } default: - status.RaiseLevel(resource.StatusFatal) - return status, fmt.Errorf("user: unrecognized state %v", u.State) + u.Status.RaiseLevel(resource.StatusFatal) + return u, fmt.Errorf("user: unrecognized state %v", u.State) } - return status, nil + return u.Status, nil } // Apply changes for user @@ -200,58 +196,45 @@ func (u *User) Apply() (resource.TaskStatus, error) { userByID, uidErr = u.system.LookupID(u.UID) } - status := &resource.Status{} + u.Status = resource.NewStatus() if nameErr == ErrUnsupported { - status.RaiseLevel(resource.StatusFatal) - return status, ErrUnsupported + u.Status.RaiseLevel(resource.StatusFatal) + return u, ErrUnsupported } switch u.State { case StatePresent: - switch { - case u.UID == "": - _, nameNotFound := nameErr.(user.UnknownUserError) + _, nameNotFound := nameErr.(user.UnknownUserError) - switch { - case nameNotFound: - options, err := SetAddUserOptions(u) - if err != nil { - status.RaiseLevel(resource.StatusCantChange) - return status, errors.Wrapf(err, "will not attempt to add user %s", u.Username) - } + switch { + case nameNotFound: + options, err := u.DiffAdd(u.Status) + if err != nil { + return u, errors.Wrapf(err, "will not attempt to add user %s", u.Username) + } + if resource.AnyChanges(u.Status.Differences) { err = u.system.AddUser(u.Username, options) if err != nil { - status.RaiseLevel(resource.StatusFatal) - status.AddMessage(fmt.Sprintf("error adding user %s", u.Username)) - return status, errors.Wrap(err, "user add") + u.Status.RaiseLevel(resource.StatusFatal) + u.Status.AddMessage(fmt.Sprintf("error adding user %s", u.Username)) + return u, errors.Wrap(err, "user add") } - status.AddMessage(fmt.Sprintf("added user %s", u.Username)) - default: - status.RaiseLevel(resource.StatusCantChange) - return status, fmt.Errorf("will not attempt to add user %s", u.Username) + u.Status.AddMessage(fmt.Sprintf("added user %s", u.Username)) } - case u.UID != "": - _, nameNotFound := nameErr.(user.UnknownUserError) - _, uidNotFound := uidErr.(user.UnknownUserIdError) - - switch { - case nameNotFound && uidNotFound: - options, err := SetAddUserOptions(u) - if err != nil { - status.RaiseLevel(resource.StatusCantChange) - return status, errors.Wrapf(err, "will not attempt to add user %s with uid %s", u.Username, u.UID) - } - err = u.system.AddUser(u.Username, options) + case userByName != nil: + options, err := u.DiffMod(u.Status, userByName) + if err != nil { + return u, errors.Wrapf(err, "will not attempt to modify user %s", u.Username) + } + if resource.AnyChanges(u.Status.Differences) { + err = u.system.ModUser(u.Username, options) if err != nil { - status.RaiseLevel(resource.StatusFatal) - status.AddMessage(fmt.Sprintf("error adding user %s with uid %s", u.Username, u.UID)) - return status, errors.Wrap(err, "user add") + u.Status.RaiseLevel(resource.StatusFatal) + u.Status.AddMessage(fmt.Sprintf("error modifying user %s", u.Username)) + return u, errors.Wrap(err, "user modify") } - status.AddMessage(fmt.Sprintf("added user %s with uid %s", u.Username, u.UID)) - default: - status.RaiseLevel(resource.StatusCantChange) - return status, fmt.Errorf("will not attempt to add user %s with uid %s", u.Username, u.UID) + u.Status.AddMessage(fmt.Sprintf("modified user %s", u.Username)) } } case StateAbsent: @@ -263,14 +246,14 @@ func (u *User) Apply() (resource.TaskStatus, error) { case !nameNotFound && userByName != nil: err := u.system.DelUser(u.Username) if err != nil { - status.RaiseLevel(resource.StatusFatal) - status.AddMessage(fmt.Sprintf("error deleting user %s", u.Username)) - return status, errors.Wrap(err, "user delete") + u.Status.RaiseLevel(resource.StatusFatal) + u.Status.AddMessage(fmt.Sprintf("error deleting user %s", u.Username)) + return u, errors.Wrap(err, "user delete") } - status.AddMessage(fmt.Sprintf("deleted user %s", u.Username)) + u.Status.AddMessage(fmt.Sprintf("deleted user %s", u.Username)) default: - status.RaiseLevel(resource.StatusCantChange) - return status, fmt.Errorf("will not attempt to delete user %s", u.Username) + u.Status.RaiseLevel(resource.StatusCantChange) + return u, fmt.Errorf("will not attempt to delete user %s", u.Username) } case u.UID != "": _, nameNotFound := nameErr.(user.UnknownUserError) @@ -280,56 +263,188 @@ func (u *User) Apply() (resource.TaskStatus, error) { case !nameNotFound && !uidNotFound && userByName != nil && userByID != nil && *userByName == *userByID: err := u.system.DelUser(u.Username) if err != nil { - status.RaiseLevel(resource.StatusFatal) - status.AddMessage(fmt.Sprintf("error deleting user %s with uid %s", u.Username, u.UID)) - return status, errors.Wrap(err, "user delete") + u.Status.RaiseLevel(resource.StatusFatal) + u.Status.AddMessage(fmt.Sprintf("error deleting user %s with uid %s", u.Username, u.UID)) + return u, errors.Wrap(err, "user delete") } - status.AddMessage(fmt.Sprintf("deleted user %s with uid %s", u.Username, u.UID)) + u.Status.AddMessage(fmt.Sprintf("deleted user %s with uid %s", u.Username, u.UID)) default: - status.RaiseLevel(resource.StatusCantChange) - return status, fmt.Errorf("will not attempt to delete user %s with uid %s", u.Username, u.UID) + u.Status.RaiseLevel(resource.StatusCantChange) + return u, fmt.Errorf("will not attempt to delete user %s with uid %s", u.Username, u.UID) } } default: - status.RaiseLevel(resource.StatusFatal) - return status, fmt.Errorf("user: unrecognized state %s", u.State) + u.Status.RaiseLevel(resource.StatusFatal) + return u, fmt.Errorf("user: unrecognized state %s", u.State) } - return status, nil + return u, nil +} + +func noOptionsSet(u *User) bool { + switch { + case u.UID != "", u.GroupName != "", u.GID != "", u.Name != "", u.HomeDir != "": + return false + } + return true } -// SetAddUserOptions returns a AddUserOptions struct with the options -// specified in the configuration for adding a user -// If group information is provided and the group is not found, nil and an -// error indicating the group name or gid is not found is returned -func SetAddUserOptions(u *User) (*AddUserOptions, error) { +// DiffAdd checks for differences between the current and desired state for the +// user to be added indicated by the User fields. The options to be used for the +// add command are set. +func (u *User) DiffAdd(status *resource.Status) (*AddUserOptions, error) { options := new(AddUserOptions) + // if a group exists with the same name as the user being added, a groupname + // must also be indicated so the user may be added to that group + grp, _ := user.LookupGroup(u.Username) + if grp != nil && grp.Name == u.Username && u.GroupName == "" { + u.Status.RaiseLevel(resource.StatusCantChange) + u.Status.AddMessage("if you want to add this user to that group, use the groupname field") + return nil, fmt.Errorf("group %s exists", u.Username) + } + u.Status.AddDifference("username", fmt.Sprintf("<%s>", string(StateAbsent)), u.Username, "") + if u.UID != "" { - options.UID = u.UID + usr, err := user.LookupId(u.UID) + _, uidNotFound := err.(user.UnknownUserIdError) + + if uidNotFound { + options.UID = u.UID + status.AddDifference("uid", fmt.Sprintf("<%s>", string(StateAbsent)), u.UID, "") + } else if usr != nil { + u.Status.RaiseLevel(resource.StatusCantChange) + return nil, fmt.Errorf("uid %s already exists", u.UID) + } } switch { case u.GroupName != "": - _, err := user.LookupGroup(u.GroupName) + grp, err := user.LookupGroup(u.GroupName) if err != nil { + u.Status.RaiseLevel(resource.StatusCantChange) return nil, fmt.Errorf("group %s does not exist", u.GroupName) + } else if grp != nil { + options.Group = u.GroupName + status.AddDifference("group", fmt.Sprintf("<%s>", string(StateAbsent)), u.GroupName, "") } - options.Group = u.GroupName case u.GID != "": - _, err := user.LookupGroupId(u.GID) + grp, err := user.LookupGroupId(u.GID) if err != nil { + u.Status.RaiseLevel(resource.StatusCantChange) return nil, fmt.Errorf("group gid %s does not exist", u.GID) + } else if grp != nil { + options.Group = u.GID + status.AddDifference("gid", fmt.Sprintf("<%s>", string(StateAbsent)), u.GID, "") } - options.Group = u.GID } if u.Name != "" { options.Comment = u.Name + status.AddDifference("comment", fmt.Sprintf("<%s>", string(StateAbsent)), u.Name, "") + } + + if u.CreateHome { + dirDiff := u.HomeDir + if u.HomeDir == "" { + dirDiff = "" + } + options.CreateHome = true + status.AddDifference("create_home", fmt.Sprintf("<%s>", string(StateAbsent)), dirDiff, "") + if u.SkelDir != "" { + options.SkelDir = u.SkelDir + status.AddDifference("skel_dir contents", u.SkelDir, dirDiff, "") + } } if u.HomeDir != "" { options.Directory = u.HomeDir + status.AddDifference("home_dir name", "", u.HomeDir, "") + } + + if resource.AnyChanges(u.Status.Differences) { + u.Status.RaiseLevel(resource.StatusWillChange) + } + + return options, nil +} + +// DiffMod checks for differences between the user associated with u.Username +// and the desired modifications of that user indicated by the other User +// fields. The options to be used for the modify command are set. +func (u *User) DiffMod(status *resource.Status, currUser *user.User) (*ModUserOptions, error) { + options := new(ModUserOptions) + + // Check for differences between currUser and the desired modifications + if u.NewUsername != "" { + usr, _ := user.Lookup(u.NewUsername) + if usr != nil { + u.Status.RaiseLevel(resource.StatusCantChange) + return nil, fmt.Errorf("user %s already exists", u.NewUsername) + } + options.Username = u.NewUsername + status.AddDifference("username", u.Username, u.NewUsername, "") + } + + if u.UID != "" { + usr, err := user.LookupId(u.UID) + _, uidNotFound := err.(user.UnknownUserIdError) + + if uidNotFound { + options.UID = u.UID + status.AddDifference("uid", currUser.Uid, u.UID, "") + } else if usr != nil && currUser.Uid != u.UID { + u.Status.RaiseLevel(resource.StatusCantChange) + return nil, fmt.Errorf("uid %s already exists", u.UID) + } + } + + switch { + case u.GroupName != "": + grp, err := user.LookupGroup(u.GroupName) + if err != nil { + u.Status.RaiseLevel(resource.StatusCantChange) + return nil, fmt.Errorf("group %s does not exist", u.GroupName) + } else if grp != nil && currUser.Gid != grp.Gid { + currGroup, err := user.LookupGroupId(currUser.Gid) + if err != nil { + u.Status.RaiseLevel(resource.StatusCantChange) + return nil, fmt.Errorf("group gid %s does not exist", currUser.Gid) + } + options.Group = u.GroupName + status.AddDifference("group", currGroup.Name, u.GroupName, "") + } + case u.GID != "": + grp, err := user.LookupGroupId(u.GID) + if err != nil { + u.Status.RaiseLevel(resource.StatusCantChange) + return nil, fmt.Errorf("group gid %s does not exist", u.GID) + } else if grp != nil && currUser.Gid != u.GID { + options.Group = u.GID + status.AddDifference("gid", currUser.Gid, u.GID, "") + } + } + + if u.Name != "" { + if currUser.Name != u.Name { + options.Comment = u.Name + status.AddDifference("comment", currUser.Name, u.Name, "") + } + } + + if u.HomeDir != "" { + if currUser.HomeDir != u.HomeDir { + options.Directory = u.HomeDir + status.AddDifference("home_dir name", currUser.HomeDir, u.HomeDir, "") + if u.MoveDir { + options.MoveDir = true + status.AddDifference("home_dir contents", currUser.HomeDir, u.HomeDir, "") + } + } + } + + if resource.AnyChanges(u.Status.Differences) { + u.Status.RaiseLevel(resource.StatusWillChange) } return options, nil diff --git a/resource/user/user_default.go b/resource/user/user_default.go index fa23de6bd..c44f1d511 100644 --- a/resource/user/user_default.go +++ b/resource/user/user_default.go @@ -33,6 +33,11 @@ func (s *System) DelUser(userName string) error { return ErrUnsupported } +// ModUser implementation for systems which are not supported +func (s *System) ModUser(userName string, options *ModUserOptions) error { + return ErrUnsupported +} + // Lookup implementation for systems which are not supported func (s *System) Lookup(userName string) (*user.User, error) { return nil, ErrUnsupported diff --git a/resource/user/user_linux.go b/resource/user/user_linux.go index b2202d6aa..4b88c27b8 100644 --- a/resource/user/user_linux.go +++ b/resource/user/user_linux.go @@ -37,6 +37,12 @@ func (s *System) AddUser(userName string, options *AddUserOptions) error { if options.Comment != "" { args = append(args, "-c", options.Comment) } + if options.CreateHome { + args = append(args, "-m") + if options.SkelDir != "" { + args = append(args, "-k", options.SkelDir) + } + } if options.Directory != "" { args = append(args, "-d", options.Directory) } @@ -59,6 +65,36 @@ func (s *System) DelUser(userName string) error { return nil } +// ModUser modifies a user +func (s *System) ModUser(userName string, options *ModUserOptions) error { + args := []string{userName} + if options.Username != "" { + args = append(args, "-l", options.Username) + } + if options.UID != "" { + args = append(args, "-u", options.UID) + } + if options.Group != "" { + args = append(args, "-g", options.Group) + } + if options.Comment != "" { + args = append(args, "-c", options.Comment) + } + if options.Directory != "" { + args = append(args, "-d", options.Directory) + if options.MoveDir { + args = append(args, "-m") + } + } + + cmd := exec.Command("usermod", args...) + err := cmd.Run() + if err != nil { + return fmt.Errorf("usermod: %s", err) + } + return nil +} + // Lookup looks up a user by name // If the user cannot be found an error is returned func (s *System) Lookup(userName string) (*user.User, error) { diff --git a/resource/user/user_test.go b/resource/user/user_test.go index 493f585bc..8b5921dde 100644 --- a/resource/user/user_test.go +++ b/resource/user/user_test.go @@ -32,22 +32,24 @@ import ( ) var ( - currUser *os.User - currUsername string - currUID string - currGroup *os.Group - currGroupName string - currGID string - userErr error - groupErr error - tempUsername []string - fakeUsername string - fakeUID string - tempGroupName []string - fakeGroupName string - fakeGID string - gidErr error - uidErr error + currUser *os.User + currUsername string + currUID string + currGroup *os.Group + currGroupName string + currGID string + existingGroup *os.Group + existingGroupName string + existingGID string + existingUser *os.User + existingUID string + tempUsername []string + fakeUsername string + fakeUID string + tempGroupName []string + fakeGroupName string + fakeGID string + err error ) const ( @@ -69,34 +71,53 @@ const ( ) func init() { - currUser, userErr = os.Current() - if userErr != nil { - panic(userErr) + currUser, err = os.Current() + if err != nil { + panic(err) } currUsername = currUser.Username currUID = currUser.Uid currGID = currUser.Gid - currGroup, groupErr = os.LookupGroupId(currGID) - if groupErr != nil { - panic(groupErr) + currGroup, err = os.LookupGroupId(currGID) + if err != nil { + panic(err) } + currGroupName = currGroup.Name - fakeUID, uidErr = setFakeUid() - if uidErr != nil { - panic(uidErr) + fakeUID, err = setFakeUid() + if err != nil { + panic(err) } - fakeGID, gidErr = setFakeGid() - if gidErr != nil { - panic(gidErr) + fakeGID, err = setFakeGid() + if err != nil { + panic(err) } - currUsername = currUser.Username tempUsername = strings.Split(uuid.NewV4().String(), "-") fakeUsername = strings.Join(tempUsername[0:], "") tempGroupName = strings.Split(uuid.NewV4().String(), "-") fakeGroupName = strings.Join(tempUsername[0:], "") + + existingGID, err = setGid() + if err != nil { + panic(err) + } + existingGroup, err = os.LookupGroupId(existingGID) + if err != nil { + panic(err) + } + existingGroupName = existingGroup.Name + + existingUID, err = setUid() + if err != nil { + panic(err) + } + existingUser, err = os.LookupId(existingUID) + if err != nil { + panic(err) + } } // TestUserInterface tests that User is properly implemented @@ -114,213 +135,60 @@ func TestCheck(t *testing.T) { u := user.NewUser(new(user.System)) u.State = user.StatePresent - t.Run("uid not provided", func(t *testing.T) { - t.Run("no add-user already exists", func(t *testing.T) { - u.Username = currUsername - status, err := u.Check(fakerenderer.New()) - - if runtime.GOOS == "linux" { - assert.NoError(t, err) - assert.Equal(t, resource.StatusNoChange, status.StatusCode()) - assert.Equal(t, fmt.Sprintf("user %s already exists", u.Username), status.Messages()[0]) - assert.False(t, status.HasChanges()) - } else { - assert.EqualError(t, err, "user: not supported on this system") - } - }) - + t.Run("add tests", func(t *testing.T) { t.Run("add user", func(t *testing.T) { u.Username = fakeUsername status, err := u.Check(fakerenderer.New()) if runtime.GOOS == "linux" { assert.NoError(t, err) - assert.Equal(t, "user does not exist", status.Messages()[0]) + assert.Equal(t, "add user", status.Messages()[0]) assert.Equal(t, resource.StatusWillChange, status.StatusCode()) - assert.Equal(t, string(user.StateAbsent), status.Diffs()["user"].Original()) - assert.Equal(t, fmt.Sprintf("user %s", u.Username), status.Diffs()["user"].Current()) + assert.Equal(t, fmt.Sprintf("<%s>", string(user.StateAbsent)), status.Diffs()["username"].Original()) + assert.Equal(t, u.Username, status.Diffs()["username"].Current()) assert.True(t, status.HasChanges()) } else { assert.EqualError(t, err, "user: not supported on this system") } }) - t.Run("group provided", func(t *testing.T) { - t.Run("no add-group name does not exist", func(t *testing.T) { - u.Username = fakeUsername - u.GroupName = fakeGroupName - status, err := u.Check(fakerenderer.New()) - - if runtime.GOOS == "linux" { - assert.EqualError(t, err, fmt.Sprintf("cannot add user %s: group %s does not exist", u.Username, u.GroupName)) - assert.Equal(t, resource.StatusCantChange, status.StatusCode()) - assert.True(t, status.HasChanges()) - } else { - assert.EqualError(t, err, "user: not supported on this system") - } - }) - - t.Run("add user with group name", func(t *testing.T) { - u.Username = fakeUsername - u.GroupName = currGroupName - status, err := u.Check(fakerenderer.New()) - - if runtime.GOOS == "linux" { - assert.NoError(t, err) - assert.Equal(t, "user does not exist", status.Messages()[0]) - assert.Equal(t, resource.StatusWillChange, status.StatusCode()) - assert.Equal(t, string(user.StateAbsent), status.Diffs()["user"].Original()) - assert.Equal(t, fmt.Sprintf("user %s", u.Username), status.Diffs()["user"].Current()) - assert.True(t, status.HasChanges()) - } else { - assert.EqualError(t, err, "user: not supported on this system") - } - }) - - t.Run("no add-group gid does not exist", func(t *testing.T) { - u.Username = fakeUsername - u.GID = fakeGID - status, err := u.Check(fakerenderer.New()) - - if runtime.GOOS == "linux" { - assert.EqualError(t, err, fmt.Sprintf("cannot add user %s: group gid %s does not exist", u.Username, u.GID)) - assert.Equal(t, resource.StatusCantChange, status.StatusCode()) - assert.True(t, status.HasChanges()) - } else { - assert.EqualError(t, err, "user: not supported on this system") - } - }) - - t.Run("add user with group gid", func(t *testing.T) { - u.Username = fakeUsername - u.GID = currGID - status, err := u.Check(fakerenderer.New()) - - if runtime.GOOS == "linux" { - assert.NoError(t, err) - assert.Equal(t, "user does not exist", status.Messages()[0]) - assert.Equal(t, resource.StatusWillChange, status.StatusCode()) - assert.Equal(t, string(user.StateAbsent), status.Diffs()["user"].Original()) - assert.Equal(t, fmt.Sprintf("user %s", u.Username), status.Diffs()["user"].Current()) - assert.True(t, status.HasChanges()) - } else { - assert.EqualError(t, err, "user: not supported on this system") - } - }) - }) - }) - - t.Run("uid provided", func(t *testing.T) { - t.Run("add user with uid", func(t *testing.T) { + t.Run("cannot add user", func(t *testing.T) { u.Username = fakeUsername - u.UID = fakeUID + u.GroupName = fakeGroupName status, err := u.Check(fakerenderer.New()) if runtime.GOOS == "linux" { - assert.NoError(t, err) - assert.Equal(t, resource.StatusWillChange, status.StatusCode()) - assert.Equal(t, fmt.Sprintf("user name and uid do not exist"), status.Messages()[0]) - assert.Equal(t, string(user.StateAbsent), status.Diffs()["user"].Original()) - assert.Equal(t, fmt.Sprintf("user %s with uid %s", u.Username, u.UID), status.Diffs()["user"].Current()) + assert.EqualError(t, err, fmt.Sprintf("cannot add user %s: group %s does not exist", u.Username, u.GroupName)) + assert.Equal(t, resource.StatusCantChange, status.StatusCode()) assert.True(t, status.HasChanges()) } else { assert.EqualError(t, err, "user: not supported on this system") } }) + }) - t.Run("group provided", func(t *testing.T) { - t.Run("no add-group name does not exist", func(t *testing.T) { - u.Username = fakeUsername - u.UID = fakeUID - u.GroupName = fakeGroupName - status, err := u.Check(fakerenderer.New()) - - if runtime.GOOS == "linux" { - assert.EqualError(t, err, fmt.Sprintf("cannot add user %s with uid %s: group %s does not exist", u.Username, u.UID, u.GroupName)) - assert.Equal(t, resource.StatusCantChange, status.StatusCode()) - assert.True(t, status.HasChanges()) - } else { - assert.EqualError(t, err, "user: not supported on this system") - } - }) - - t.Run("add user with group name", func(t *testing.T) { - u.Username = fakeUsername - u.UID = fakeUID - u.GroupName = currGroupName - status, err := u.Check(fakerenderer.New()) - - if runtime.GOOS == "linux" { - assert.NoError(t, err) - assert.Equal(t, resource.StatusWillChange, status.StatusCode()) - assert.Equal(t, fmt.Sprintf("user name and uid do not exist"), status.Messages()[0]) - assert.Equal(t, string(user.StateAbsent), status.Diffs()["user"].Original()) - assert.Equal(t, fmt.Sprintf("user %s with uid %s", u.Username, u.UID), status.Diffs()["user"].Current()) - assert.True(t, status.HasChanges()) - } else { - assert.EqualError(t, err, "user: not supported on this system") - } - }) - - t.Run("no add-group gid does not exist", func(t *testing.T) { - u.Username = fakeUsername - u.UID = fakeUID - u.GID = fakeGID - status, err := u.Check(fakerenderer.New()) - - if runtime.GOOS == "linux" { - assert.EqualError(t, err, fmt.Sprintf("cannot add user %s with uid %s: group gid %s does not exist", u.Username, u.UID, u.GID)) - assert.Equal(t, resource.StatusCantChange, status.StatusCode()) - assert.True(t, status.HasChanges()) - } else { - assert.EqualError(t, err, "user: not supported on this system") - } - }) - - t.Run("add user with group gid", func(t *testing.T) { - gid, err := setGid() - if err != nil { - panic(err) - } - u.Username = fakeUsername - u.UID = fakeUID - u.GID = gid - status, err := u.Check(fakerenderer.New()) - - if runtime.GOOS == "linux" { - assert.NoError(t, err) - assert.Equal(t, resource.StatusWillChange, status.StatusCode()) - assert.Equal(t, fmt.Sprintf("user name and uid do not exist"), status.Messages()[0]) - assert.Equal(t, string(user.StateAbsent), status.Diffs()["user"].Original()) - assert.Equal(t, fmt.Sprintf("user %s with uid %s", u.Username, u.UID), status.Diffs()["user"].Current()) - assert.True(t, status.HasChanges()) - } else { - assert.EqualError(t, err, "user: not supported on this system") - } - }) - }) - - t.Run("no add-user uid already exists", func(t *testing.T) { - u.Username = fakeUsername - u.UID = currUID + t.Run("modify tests", func(t *testing.T) { + t.Run("no modifications", func(t *testing.T) { + u.Username = currUsername + u.GroupName = "" // clear this field set from previous t.Run status, err := u.Check(fakerenderer.New()) if runtime.GOOS == "linux" { - assert.EqualError(t, err, fmt.Sprintf("cannot add user %s with uid %s: user uid already exists", u.Username, u.UID)) - assert.Equal(t, resource.StatusCantChange, status.StatusCode()) - assert.True(t, status.HasChanges()) + assert.NoError(t, err) + assert.Equal(t, resource.StatusNoChange, status.StatusCode()) + assert.False(t, status.HasChanges()) } else { assert.EqualError(t, err, "user: not supported on this system") } }) - t.Run("no add-user name already exists", func(t *testing.T) { + t.Run("cannot modify user", func(t *testing.T) { u.Username = currUsername - u.UID = fakeUID + u.GroupName = fakeGroupName status, err := u.Check(fakerenderer.New()) if runtime.GOOS == "linux" { - assert.EqualError(t, err, fmt.Sprintf("cannot add user %s with uid %s: user already exists", u.Username, u.UID)) + assert.EqualError(t, err, fmt.Sprintf("cannot modify user %s: group %s does not exist", u.Username, u.GroupName)) assert.Equal(t, resource.StatusCantChange, status.StatusCode()) assert.True(t, status.HasChanges()) } else { @@ -328,18 +196,18 @@ func TestCheck(t *testing.T) { } }) - t.Run("no add-user name and uid belong to different users", func(t *testing.T) { - uid, err := setUid() - if err != nil { - panic(err) - } + t.Run("modify user", func(t *testing.T) { u.Username = currUsername - u.UID = uid + u.NewUsername = fakeUsername + u.GroupName = "" // clear this field set from previous t.Run status, err := u.Check(fakerenderer.New()) if runtime.GOOS == "linux" { - assert.EqualError(t, err, fmt.Sprintf("cannot add user %s with uid %s: user and uid belong to different users", u.Username, u.UID)) - assert.Equal(t, resource.StatusCantChange, status.StatusCode()) + assert.NoError(t, err) + assert.Equal(t, resource.StatusWillChange, status.StatusCode()) + assert.Equal(t, "modify user", status.Messages()[0]) + assert.Equal(t, u.Username, status.Diffs()["username"].Original()) + assert.Equal(t, u.NewUsername, status.Diffs()["username"].Current()) assert.True(t, status.HasChanges()) } else { assert.EqualError(t, err, "user: not supported on this system") @@ -375,7 +243,7 @@ func TestCheck(t *testing.T) { assert.NoError(t, err) assert.Equal(t, resource.StatusWillChange, status.StatusCode()) assert.Equal(t, fmt.Sprintf("user %s", u.Username), status.Diffs()["user"].Original()) - assert.Equal(t, string(user.StateAbsent), status.Diffs()["user"].Current()) + assert.Equal(t, fmt.Sprintf("<%s>", string(user.StateAbsent)), status.Diffs()["user"].Current()) assert.True(t, status.HasChanges()) } else { assert.EqualError(t, err, "user: not supported on this system") @@ -428,12 +296,8 @@ func TestCheck(t *testing.T) { }) t.Run("no delete-user name and uid belong to different users", func(t *testing.T) { - uid, err := setUid() - if err != nil { - panic(err) - } u.Username = currUsername - u.UID = uid + u.UID = existingUID status, err := u.Check(fakerenderer.New()) if runtime.GOOS == "linux" { @@ -454,7 +318,7 @@ func TestCheck(t *testing.T) { assert.NoError(t, err) assert.Equal(t, resource.StatusWillChange, status.StatusCode()) assert.Equal(t, fmt.Sprintf("user %s with uid %s", u.Username, u.UID), status.Diffs()["user"].Original()) - assert.Equal(t, string(user.StateAbsent), status.Diffs()["user"].Current()) + assert.Equal(t, fmt.Sprintf("<%s>", string(user.StateAbsent)), status.Diffs()["user"].Current()) assert.True(t, status.HasChanges()) } else { assert.EqualError(t, err, "user: not supported on this system") @@ -485,20 +349,20 @@ func TestApply(t *testing.T) { t.Parallel() t.Run("state=present", func(t *testing.T) { - t.Run("uid not provided", func(t *testing.T) { + t.Run("add tests", func(t *testing.T) { t.Run("add user", func(t *testing.T) { usr := &os.User{ Username: fakeUsername, } m := &MockSystem{} - o := &MockOptions{} + d := &MockDiff{} u := user.NewUser(m) u.Username = usr.Username u.State = user.StatePresent options := user.AddUserOptions{} m.On("Lookup", u.Username).Return(usr, os.UnknownUserError("")) - o.On("SetAddUserOptions", u).Return(options, nil) + d.On("DiffAdd", u.Status).Return(options, nil) m.On("AddUser", u.Username, &options).Return(nil) status, err := u.Apply() @@ -507,7 +371,7 @@ func TestApply(t *testing.T) { assert.Equal(t, fmt.Sprintf("added user %s", u.Username), status.Messages()[0]) }) - t.Run("no add-group name does not exist", func(t *testing.T) { + t.Run("will not attempt to add", func(t *testing.T) { usr := &os.User{ Username: fakeUsername, } @@ -515,7 +379,7 @@ func TestApply(t *testing.T) { Name: fakeGroupName, } m := &MockSystem{} - o := &MockOptions{} + d := &MockDiff{} u := user.NewUser(m) u.Username = usr.Username u.GroupName = grp.Name @@ -525,7 +389,7 @@ func TestApply(t *testing.T) { m.On("Lookup", u.Username).Return(usr, os.UnknownUserError("")) m.On("LookupGroup", u.GroupName).Return(grp, os.UnknownGroupError("")) - o.On("SetAddUserOptions", u).Return(nil, optErr) + d.On("DiffAdd", u.Status).Return(nil, optErr) m.On("AddUser", u.Username, &options).Return(nil) status, err := u.Apply() @@ -534,207 +398,100 @@ func TestApply(t *testing.T) { assert.Equal(t, resource.StatusCantChange, status.StatusCode()) }) - t.Run("no add-group gid does not exist", func(t *testing.T) { + t.Run("error adding user", func(t *testing.T) { usr := &os.User{ Username: fakeUsername, } - grp := &os.Group{ - Gid: fakeGID, - } m := &MockSystem{} - o := &MockOptions{} + d := &MockDiff{} u := user.NewUser(m) u.Username = usr.Username - u.GID = grp.Gid u.State = user.StatePresent options := user.AddUserOptions{} - optErr := fmt.Sprintf("group gid %s does not exist", u.GID) m.On("Lookup", u.Username).Return(usr, os.UnknownUserError("")) - m.On("LookupGroupID", u.GID).Return(grp, os.UnknownGroupError("")) - o.On("SetAddUserOptions", u).Return(nil, optErr) - m.On("AddUser", u.Username, &options).Return(nil) - status, err := u.Apply() - - m.AssertNotCalled(t, "AddUser", u.Username, &options) - assert.EqualError(t, err, fmt.Sprintf("will not attempt to add user %s: %s", u.Username, optErr)) - assert.Equal(t, resource.StatusCantChange, status.StatusCode()) - }) - - t.Run("no add-error adding user", func(t *testing.T) { - usr := &os.User{ - Username: fakeUsername, - } - m := &MockSystem{} - o := &MockOptions{} - u := user.NewUser(m) - u.Username = usr.Username - u.State = user.StatePresent - options := user.AddUserOptions{} - - m.On("Lookup", u.Username).Return(usr, os.UnknownUserError("")) - o.On("SetAddUserOptions", u).Return(options, nil) + d.On("DiffAdd", u.Status).Return(options, nil) m.On("AddUser", u.Username, &options).Return(fmt.Errorf("")) status, err := u.Apply() m.AssertCalled(t, "AddUser", u.Username, &options) - assert.EqualError(t, err, fmt.Sprintf("user add: ")) + assert.EqualError(t, err, "user add: ") assert.Equal(t, resource.StatusFatal, status.StatusCode()) assert.Equal(t, fmt.Sprintf("error adding user %s", u.Username), status.Messages()[0]) }) - - t.Run("no add-will not attempt add", func(t *testing.T) { - usr := &os.User{ - Username: fakeUsername, - } - m := &MockSystem{} - o := &MockOptions{} - u := user.NewUser(m) - u.Username = usr.Username - u.State = user.StatePresent - options := user.AddUserOptions{} - - m.On("Lookup", u.Username).Return(usr, nil) - o.On("SetAddUserOptions", u).Return(options, nil) - m.On("AddUser", u.Username, &options).Return(nil) - status, err := u.Apply() - - m.AssertNotCalled(t, "AddUser", u.Username, &options) - assert.EqualError(t, err, fmt.Sprintf("will not attempt to add user %s", u.Username)) - assert.Equal(t, resource.StatusCantChange, status.StatusCode()) - }) }) - t.Run("uid provided", func(t *testing.T) { - t.Run("add user with uid", func(t *testing.T) { + t.Run("modify tests", func(t *testing.T) { + t.Run("modify user", func(t *testing.T) { usr := &os.User{ - Username: fakeUsername, - Uid: fakeUID, + Username: currUsername, } m := &MockSystem{} - o := &MockOptions{} + d := &MockDiff{} u := user.NewUser(m) u.Username = usr.Username - u.UID = usr.Uid + u.Name = "test" u.State = user.StatePresent - options := user.AddUserOptions{UID: u.UID} + options := user.ModUserOptions{Comment: u.Name} - m.On("Lookup", u.Username).Return(usr, os.UnknownUserError("")) - m.On("LookupID", u.UID).Return(usr, os.UnknownUserIdError(1)) - o.On("SetAddUserOptions", u).Return(options, nil) - m.On("AddUser", u.Username, &options).Return(nil) + m.On("Lookup", u.Username).Return(usr, nil) + d.On("DiffMod", u.Status, currUser).Return(options, nil) + m.On("ModUser", u.Username, &options).Return(nil) status, err := u.Apply() - m.AssertCalled(t, "AddUser", u.Username, &options) + m.AssertCalled(t, "ModUser", u.Username, &options) assert.NoError(t, err) - assert.Equal(t, fmt.Sprintf("added user %s with uid %s", u.Username, u.UID), status.Messages()[0]) + assert.Equal(t, fmt.Sprintf("modified user %s", u.Username), status.Messages()[0]) }) - t.Run("no add-group name does not exist", func(t *testing.T) { + t.Run("will not attempt to modify", func(t *testing.T) { usr := &os.User{ - Username: fakeUsername, - Uid: fakeUID, + Username: currUsername, } grp := &os.Group{ Name: fakeGroupName, } m := &MockSystem{} - o := &MockOptions{} + d := &MockDiff{} u := user.NewUser(m) u.Username = usr.Username u.GroupName = grp.Name u.State = user.StatePresent - options := user.AddUserOptions{} + options := user.ModUserOptions{} optErr := fmt.Sprintf("group %s does not exist", u.GroupName) - m.On("Lookup", u.Username).Return(usr, os.UnknownUserError("")) - m.On("LookupID", u.UID).Return(usr, os.UnknownUserIdError(1)) + m.On("Lookup", u.Username).Return(usr, nil) m.On("LookupGroup", u.GroupName).Return(grp, os.UnknownGroupError("")) - o.On("SetAddUserOptions", u).Return(nil, optErr) - m.On("AddUser", u.Username, &options).Return(nil) + d.On("DiffMod", u.Status, currUser).Return(nil, optErr) + m.On("ModUser", u.Username, &options).Return(nil) status, err := u.Apply() - m.AssertNotCalled(t, "AddUser", u.Username, &options) - assert.EqualError(t, err, fmt.Sprintf("will not attempt to add user %s: %s", u.Username, optErr)) + m.AssertNotCalled(t, "ModUser", u.Username, &options) + assert.EqualError(t, err, fmt.Sprintf("will not attempt to modify user %s: %s", u.Username, optErr)) assert.Equal(t, resource.StatusCantChange, status.StatusCode()) }) - t.Run("no add-group gid does not exist", func(t *testing.T) { + t.Run("error modifying user", func(t *testing.T) { usr := &os.User{ - Username: fakeUsername, - Uid: fakeUID, - } - grp := &os.Group{ - Gid: fakeGID, + Username: currUsername, } m := &MockSystem{} - o := &MockOptions{} + d := &MockDiff{} u := user.NewUser(m) u.Username = usr.Username - u.GID = grp.Gid + u.Name = "test" u.State = user.StatePresent - options := user.AddUserOptions{} - optErr := fmt.Sprintf("group gid %s does not exist", u.GID) - - m.On("Lookup", u.Username).Return(usr, os.UnknownUserError("")) - m.On("LookupID", u.UID).Return(usr, os.UnknownUserIdError(1)) - m.On("LookupGroupID", u.GID).Return(grp, os.UnknownGroupError("")) - o.On("SetAddUserOptions", u).Return(nil, optErr) - m.On("AddUser", u.Username, &options).Return(nil) - status, err := u.Apply() - - m.AssertNotCalled(t, "AddUser", u.Username, &options) - assert.EqualError(t, err, fmt.Sprintf("will not attempt to add user %s: %s", u.Username, optErr)) - assert.Equal(t, resource.StatusCantChange, status.StatusCode()) - }) - - t.Run("no add-error adding user", func(t *testing.T) { - usr := &os.User{ - Username: fakeUsername, - Uid: fakeUID, - } - m := &MockSystem{} - o := &MockOptions{} - u := user.NewUser(m) - u.Username = usr.Username - u.UID = usr.Uid - u.State = user.StatePresent - options := user.AddUserOptions{UID: u.UID} - - m.On("Lookup", u.Username).Return(usr, os.UnknownUserError("")) - m.On("LookupID", u.UID).Return(usr, os.UnknownUserIdError(1)) - o.On("SetAddUserOptions", u).Return(options, nil) - m.On("AddUser", u.Username, &options).Return(fmt.Errorf("")) - status, err := u.Apply() - - m.AssertCalled(t, "AddUser", u.Username, &options) - assert.EqualError(t, err, fmt.Sprintf("user add: ")) - assert.Equal(t, resource.StatusFatal, status.StatusCode()) - assert.Equal(t, fmt.Sprintf("error adding user %s with uid %s", u.Username, u.UID), status.Messages()[0]) - }) - - t.Run("no add-will not attempt add", func(t *testing.T) { - usr := &os.User{ - Username: fakeUsername, - Uid: fakeUID, - } - m := &MockSystem{} - o := &MockOptions{} - u := user.NewUser(m) - u.Username = usr.Username - u.UID = usr.Uid - u.State = user.StatePresent - options := user.AddUserOptions{UID: u.UID} + options := user.ModUserOptions{Comment: u.Name} m.On("Lookup", u.Username).Return(usr, nil) - m.On("LookupID", u.UID).Return(usr, nil) - o.On("SetAddUserOptions", u).Return(options, nil) - m.On("AddUser", u.Username, &options).Return(nil) + d.On("DiffMod", u.Status, currUser).Return(options, nil) + m.On("ModUser", u.Username, &options).Return(fmt.Errorf("")) status, err := u.Apply() - m.AssertNotCalled(t, "AddUser", u.Username, &options) - assert.EqualError(t, err, fmt.Sprintf("will not attempt to add user %s with uid %s", u.Username, u.UID)) - assert.Equal(t, resource.StatusCantChange, status.StatusCode()) + m.AssertCalled(t, "ModUser", u.Username, &options) + assert.EqualError(t, err, "user modify: ") + assert.Equal(t, resource.StatusFatal, status.StatusCode()) + assert.Equal(t, fmt.Sprintf("error modifying user %s", u.Username), status.Messages()[0]) }) }) }) @@ -773,7 +530,7 @@ func TestApply(t *testing.T) { status, err := u.Apply() m.AssertCalled(t, "DelUser", u.Username) - assert.EqualError(t, err, fmt.Sprintf("user delete: ")) + assert.EqualError(t, err, "user delete: ") assert.Equal(t, resource.StatusFatal, status.StatusCode()) assert.Equal(t, fmt.Sprintf("error deleting user %s", u.Username), status.Messages()[0]) }) @@ -836,7 +593,7 @@ func TestApply(t *testing.T) { status, err := u.Apply() m.AssertCalled(t, "DelUser", u.Username) - assert.EqualError(t, err, fmt.Sprintf("user delete: ")) + assert.EqualError(t, err, "user delete: ") assert.Equal(t, resource.StatusFatal, status.StatusCode()) assert.Equal(t, fmt.Sprintf("error deleting user %s with uid %s", u.Username, u.UID), status.Messages()[0]) }) @@ -870,7 +627,7 @@ func TestApply(t *testing.T) { Uid: fakeUID, } m := &MockSystem{} - o := &MockOptions{} + d := &MockDiff{} u := user.NewUser(m) u.Username = usr.Username u.UID = usr.Uid @@ -879,12 +636,12 @@ func TestApply(t *testing.T) { m.On("Lookup", u.Username).Return(usr, nil) m.On("LookupID", u.UID).Return(usr, nil) - o.On("SetAddUserOptions", u).Return(options, nil) + d.On("DiffAdd", u.Status).Return(options, nil) m.On("AddUser", u.Username, &options).Return(nil) m.On("DelUser", u.Username).Return(nil) status, err := u.Apply() - o.AssertNotCalled(t, "SetAddUserOptions", u) + d.AssertNotCalled(t, "DiffAdd", u) m.AssertNotCalled(t, "AddUser", u.Username, &options) m.AssertNotCalled(t, "DelUser", u.Username) assert.EqualError(t, err, fmt.Sprintf("user: unrecognized state %s", u.State)) @@ -892,105 +649,662 @@ func TestApply(t *testing.T) { }) } -// TestSetAddUserOptions tests options provided for adding a user -// are properly set -func TestSetAddUserOptions(t *testing.T) { +// TestDiffAdd tests DiffAdd for user +func TestDiffAdd(t *testing.T) { t.Parallel() - gid, err := setGid() - if err != nil { - panic(err) - } - grp, err := os.LookupGroupId(gid) - if err != nil { - panic(err) - } - t.Run("all options", func(t *testing.T) { + t.Run("set all options", func(t *testing.T) { u := user.NewUser(new(user.System)) u.Username = fakeUsername u.UID = fakeUID - u.GID = gid + u.GroupName = existingGroupName u.Name = "test" - u.HomeDir = "testDir" + u.CreateHome = true + u.SkelDir = "/tmp/skel" + u.HomeDir = "/tmp/test" + u.Status = resource.NewStatus() + + expected := &user.AddUserOptions{ + UID: u.UID, + Group: u.GroupName, + Comment: u.Name, + CreateHome: u.CreateHome, + SkelDir: u.SkelDir, + Directory: u.HomeDir, + } - options, err := user.SetAddUserOptions(u) + options, err := u.DiffAdd(u.Status) assert.NoError(t, err) - assert.Equal(t, u.UID, options.UID) - assert.Equal(t, u.GID, options.Group) - assert.Equal(t, u.Name, options.Comment) - assert.Equal(t, u.HomeDir, options.Directory) + assert.Equal(t, expected, options) + assert.Equal(t, resource.StatusWillChange, u.Status.StatusCode()) + assert.True(t, u.Status.HasChanges()) + assert.Equal(t, fmt.Sprintf("<%s>", string(user.StateAbsent)), u.Status.Diffs()["username"].Original()) + assert.Equal(t, u.Username, u.Status.Diffs()["username"].Current()) + assert.Equal(t, fmt.Sprintf("<%s>", string(user.StateAbsent)), u.Status.Diffs()["group"].Original()) + assert.Equal(t, u.GroupName, u.Status.Diffs()["group"].Current()) + assert.Equal(t, fmt.Sprintf("<%s>", string(user.StateAbsent)), u.Status.Diffs()["uid"].Original()) + assert.Equal(t, u.UID, u.Status.Diffs()["uid"].Current()) + assert.Equal(t, fmt.Sprintf("<%s>", string(user.StateAbsent)), u.Status.Diffs()["comment"].Original()) + assert.Equal(t, u.Name, u.Status.Diffs()["comment"].Current()) + assert.Equal(t, fmt.Sprintf("<%s>", string(user.StateAbsent)), u.Status.Diffs()["create_home"].Original()) + assert.Equal(t, u.HomeDir, u.Status.Diffs()["create_home"].Current()) + assert.Equal(t, u.SkelDir, u.Status.Diffs()["skel_dir contents"].Original()) + assert.Equal(t, u.HomeDir, u.Status.Diffs()["skel_dir contents"].Current()) + assert.Equal(t, "", u.Status.Diffs()["home_dir name"].Original()) + assert.Equal(t, u.HomeDir, u.Status.Diffs()["home_dir name"].Current()) }) - t.Run("group options", func(t *testing.T) { - t.Run("group name and gid", func(t *testing.T) { + t.Run("username", func(t *testing.T) { + t.Run("group exists-provide groupname", func(t *testing.T) { + u := user.NewUser(new(user.System)) + u.Username = existingGroupName + u.GroupName = existingGroupName + u.Status = resource.NewStatus() + + expected := &user.AddUserOptions{ + Group: u.GroupName, + } + + options, err := u.DiffAdd(u.Status) + + assert.NoError(t, err) + assert.Equal(t, expected, options) + assert.Equal(t, resource.StatusWillChange, u.Status.StatusCode()) + assert.True(t, u.Status.HasChanges()) + assert.Equal(t, fmt.Sprintf("<%s>", string(user.StateAbsent)), u.Status.Diffs()["username"].Original()) + assert.Equal(t, u.Username, u.Status.Diffs()["username"].Current()) + assert.Equal(t, fmt.Sprintf("<%s>", string(user.StateAbsent)), u.Status.Diffs()["group"].Original()) + assert.Equal(t, u.GroupName, u.Status.Diffs()["group"].Current()) + }) + + t.Run("error-group exists", func(t *testing.T) { + u := user.NewUser(new(user.System)) + u.Username = existingGroupName + u.Status = resource.NewStatus() + + options, err := u.DiffAdd(u.Status) + + assert.EqualError(t, err, fmt.Sprintf("group %s exists", u.Username)) + assert.Nil(t, options) + assert.Equal(t, resource.StatusCantChange, u.Status.StatusCode()) + assert.Equal(t, "if you want to add this user to that group, use the groupname field", u.Status.Messages()[0]) + assert.True(t, u.Status.HasChanges()) + }) + }) + + t.Run("uid", func(t *testing.T) { + t.Run("uid not found", func(t *testing.T) { u := user.NewUser(new(user.System)) u.Username = fakeUsername - u.GroupName = grp.Name - u.GID = gid + u.UID = fakeUID + u.Status = resource.NewStatus() + + expected := &user.AddUserOptions{ + UID: u.UID, + } - options, err := user.SetAddUserOptions(u) + options, err := u.DiffAdd(u.Status) assert.NoError(t, err) - assert.Equal(t, u.GroupName, options.Group) + assert.Equal(t, expected, options) + assert.Equal(t, resource.StatusWillChange, u.Status.StatusCode()) + assert.True(t, u.Status.HasChanges()) + assert.Equal(t, fmt.Sprintf("<%s>", string(user.StateAbsent)), u.Status.Diffs()["username"].Original()) + assert.Equal(t, u.Username, u.Status.Diffs()["username"].Current()) + assert.Equal(t, fmt.Sprintf("<%s>", string(user.StateAbsent)), u.Status.Diffs()["uid"].Original()) + assert.Equal(t, u.UID, u.Status.Diffs()["uid"].Current()) }) - t.Run("with group name", func(t *testing.T) { + t.Run("error-uid found", func(t *testing.T) { u := user.NewUser(new(user.System)) u.Username = fakeUsername - u.GroupName = grp.Name + u.UID = currUID + u.Status = resource.NewStatus() - options, err := user.SetAddUserOptions(u) + options, err := u.DiffAdd(u.Status) + + assert.EqualError(t, err, fmt.Sprintf("uid %s already exists", u.UID)) + assert.Nil(t, options) + assert.Equal(t, resource.StatusCantChange, u.Status.StatusCode()) + assert.True(t, u.Status.HasChanges()) + }) + }) + + t.Run("group", func(t *testing.T) { + t.Run("with groupname", func(t *testing.T) { + u := user.NewUser(new(user.System)) + u.Username = fakeUsername + u.GroupName = existingGroupName + u.Status = resource.NewStatus() + + expected := &user.AddUserOptions{ + Group: u.GroupName, + } + + options, err := u.DiffAdd(u.Status) assert.NoError(t, err) - assert.Equal(t, u.GroupName, options.Group) + assert.Equal(t, expected, options) + assert.Equal(t, resource.StatusWillChange, u.Status.StatusCode()) + assert.True(t, u.Status.HasChanges()) + assert.Equal(t, fmt.Sprintf("<%s>", string(user.StateAbsent)), u.Status.Diffs()["username"].Original()) + assert.Equal(t, u.Username, u.Status.Diffs()["username"].Current()) + assert.Equal(t, fmt.Sprintf("<%s>", string(user.StateAbsent)), u.Status.Diffs()["group"].Original()) + assert.Equal(t, u.GroupName, u.Status.Diffs()["group"].Current()) }) - t.Run("group name not found", func(t *testing.T) { + t.Run("error-groupname not found", func(t *testing.T) { u := user.NewUser(new(user.System)) u.Username = fakeUsername u.GroupName = fakeGroupName + u.Status = resource.NewStatus() - options, err := user.SetAddUserOptions(u) + options, err := u.DiffAdd(u.Status) assert.EqualError(t, err, fmt.Sprintf("group %s does not exist", u.GroupName)) assert.Nil(t, options) + assert.Equal(t, resource.StatusCantChange, u.Status.StatusCode()) + assert.True(t, u.Status.HasChanges()) }) - t.Run("with group gid", func(t *testing.T) { + t.Run("with gid", func(t *testing.T) { u := user.NewUser(new(user.System)) u.Username = fakeUsername - u.GID = gid + u.GID = existingGID + u.Status = resource.NewStatus() - options, err := user.SetAddUserOptions(u) + expected := &user.AddUserOptions{ + Group: u.GID, + } + + options, err := u.DiffAdd(u.Status) assert.NoError(t, err) - assert.Equal(t, u.GID, options.Group) + assert.Equal(t, expected, options) + assert.Equal(t, resource.StatusWillChange, u.Status.StatusCode()) + assert.True(t, u.Status.HasChanges()) + assert.Equal(t, fmt.Sprintf("<%s>", string(user.StateAbsent)), u.Status.Diffs()["username"].Original()) + assert.Equal(t, u.Username, u.Status.Diffs()["username"].Current()) + assert.Equal(t, fmt.Sprintf("<%s>", string(user.StateAbsent)), u.Status.Diffs()["gid"].Original()) + assert.Equal(t, u.GID, u.Status.Diffs()["gid"].Current()) }) - t.Run("group gid not found", func(t *testing.T) { + t.Run("error-gid not found", func(t *testing.T) { u := user.NewUser(new(user.System)) u.Username = fakeUsername u.GID = fakeGID + u.Status = resource.NewStatus() - options, err := user.SetAddUserOptions(u) + options, err := u.DiffAdd(u.Status) assert.EqualError(t, err, fmt.Sprintf("group gid %s does not exist", u.GID)) assert.Nil(t, options) + assert.Equal(t, resource.StatusCantChange, u.Status.StatusCode()) + assert.True(t, u.Status.HasChanges()) + }) + + t.Run("user with groupname and gid", func(t *testing.T) { + u := user.NewUser(new(user.System)) + u.Username = fakeUsername + u.GroupName = existingGroupName + u.GID = existingGID + u.Status = resource.NewStatus() + + expected := &user.AddUserOptions{ + Group: u.GroupName, + } + + options, err := u.DiffAdd(u.Status) + + assert.NoError(t, err) + assert.Equal(t, expected, options) + assert.Equal(t, resource.StatusWillChange, u.Status.StatusCode()) + assert.True(t, u.Status.HasChanges()) + assert.Equal(t, fmt.Sprintf("<%s>", string(user.StateAbsent)), u.Status.Diffs()["username"].Original()) + assert.Equal(t, u.Username, u.Status.Diffs()["username"].Current()) + assert.Equal(t, fmt.Sprintf("<%s>", string(user.StateAbsent)), u.Status.Diffs()["group"].Original()) + assert.Equal(t, u.GroupName, u.Status.Diffs()["group"].Current()) + }) + }) + + t.Run("comment", func(t *testing.T) { + u := user.NewUser(new(user.System)) + u.Username = fakeUsername + u.Name = "test" + u.Status = resource.NewStatus() + + expected := &user.AddUserOptions{ + Comment: u.Name, + } + + options, err := u.DiffAdd(u.Status) + + assert.NoError(t, err) + assert.Equal(t, expected, options) + assert.Equal(t, resource.StatusWillChange, u.Status.StatusCode()) + assert.True(t, u.Status.HasChanges()) + assert.Equal(t, fmt.Sprintf("<%s>", string(user.StateAbsent)), u.Status.Diffs()["username"].Original()) + assert.Equal(t, u.Username, u.Status.Diffs()["username"].Current()) + assert.Equal(t, fmt.Sprintf("<%s>", string(user.StateAbsent)), u.Status.Diffs()["comment"].Original()) + assert.Equal(t, u.Name, u.Status.Diffs()["comment"].Current()) + }) + + t.Run("directory", func(t *testing.T) { + t.Run("create_home with home_dir", func(t *testing.T) { + u := user.NewUser(new(user.System)) + u.Username = fakeUsername + u.CreateHome = true + u.HomeDir = "/tmp/test" + u.SkelDir = "/tmp/skel" + u.Status = resource.NewStatus() + + expected := &user.AddUserOptions{ + CreateHome: u.CreateHome, + SkelDir: u.SkelDir, + Directory: u.HomeDir, + } + + options, err := u.DiffAdd(u.Status) + + assert.NoError(t, err) + assert.Equal(t, expected, options) + assert.Equal(t, resource.StatusWillChange, u.Status.StatusCode()) + assert.True(t, u.Status.HasChanges()) + assert.Equal(t, fmt.Sprintf("<%s>", string(user.StateAbsent)), u.Status.Diffs()["username"].Original()) + assert.Equal(t, u.Username, u.Status.Diffs()["username"].Current()) + assert.Equal(t, fmt.Sprintf("<%s>", string(user.StateAbsent)), u.Status.Diffs()["create_home"].Original()) + assert.Equal(t, u.HomeDir, u.Status.Diffs()["create_home"].Current()) + assert.Equal(t, "", u.Status.Diffs()["home_dir name"].Original()) + assert.Equal(t, u.HomeDir, u.Status.Diffs()["home_dir name"].Current()) + assert.Equal(t, u.SkelDir, u.Status.Diffs()["skel_dir contents"].Original()) + assert.Equal(t, u.HomeDir, u.Status.Diffs()["skel_dir contents"].Current()) + }) + + t.Run("create_home with default home", func(t *testing.T) { + u := user.NewUser(new(user.System)) + u.Username = fakeUsername + u.CreateHome = true + u.SkelDir = "/tmp/skel" + u.Status = resource.NewStatus() + + expected := &user.AddUserOptions{ + CreateHome: u.CreateHome, + SkelDir: u.SkelDir, + } + + options, err := u.DiffAdd(u.Status) + + assert.NoError(t, err) + assert.Equal(t, expected, options) + assert.Equal(t, resource.StatusWillChange, u.Status.StatusCode()) + assert.True(t, u.Status.HasChanges()) + assert.Equal(t, fmt.Sprintf("<%s>", string(user.StateAbsent)), u.Status.Diffs()["username"].Original()) + assert.Equal(t, u.Username, u.Status.Diffs()["username"].Current()) + assert.Equal(t, fmt.Sprintf("<%s>", string(user.StateAbsent)), u.Status.Diffs()["create_home"].Original()) + assert.Equal(t, "", u.Status.Diffs()["create_home"].Current()) + assert.Equal(t, u.SkelDir, u.Status.Diffs()["skel_dir contents"].Original()) + assert.Equal(t, "", u.Status.Diffs()["skel_dir contents"].Current()) + }) + + t.Run("default home without create_home", func(t *testing.T) { + u := user.NewUser(new(user.System)) + u.Username = fakeUsername + u.SkelDir = "/tmp/skel" + u.Status = resource.NewStatus() + + expected := &user.AddUserOptions{} + + options, err := u.DiffAdd(u.Status) + + assert.NoError(t, err) + assert.Equal(t, expected, options) + assert.Equal(t, resource.StatusWillChange, u.Status.StatusCode()) + assert.True(t, u.Status.HasChanges()) + assert.Equal(t, fmt.Sprintf("<%s>", string(user.StateAbsent)), u.Status.Diffs()["username"].Original()) + assert.Equal(t, u.Username, u.Status.Diffs()["username"].Current()) + }) + + t.Run("home_dir without create_home", func(t *testing.T) { + u := user.NewUser(new(user.System)) + u.Username = fakeUsername + u.HomeDir = "/tmp/test" + u.SkelDir = "/tmp/skel" + u.Status = resource.NewStatus() + + expected := &user.AddUserOptions{ + Directory: u.HomeDir, + } + + options, err := u.DiffAdd(u.Status) + + assert.NoError(t, err) + assert.Equal(t, expected, options) + assert.Equal(t, resource.StatusWillChange, u.Status.StatusCode()) + assert.True(t, u.Status.HasChanges()) + assert.Equal(t, fmt.Sprintf("<%s>", string(user.StateAbsent)), u.Status.Diffs()["username"].Original()) + assert.Equal(t, u.Username, u.Status.Diffs()["username"].Current()) + assert.Equal(t, "", u.Status.Diffs()["home_dir name"].Original()) + assert.Equal(t, u.HomeDir, u.Status.Diffs()["home_dir name"].Current()) }) }) t.Run("no options", func(t *testing.T) { u := user.NewUser(new(user.System)) u.Username = fakeUsername + u.Status = resource.NewStatus() + + expected := &user.AddUserOptions{} + + options, err := u.DiffAdd(u.Status) + + assert.NoError(t, err) + assert.Equal(t, expected, options) + assert.Equal(t, resource.StatusWillChange, u.Status.StatusCode()) + assert.True(t, u.Status.HasChanges()) + assert.Equal(t, fmt.Sprintf("<%s>", string(user.StateAbsent)), u.Status.Diffs()["username"].Original()) + assert.Equal(t, u.Username, u.Status.Diffs()["username"].Current()) + }) +} + +// TestDiffMod tests DiffMod for user +func TestDiffMod(t *testing.T) { + t.Parallel() + + t.Run("set all options", func(t *testing.T) { + u := user.NewUser(new(user.System)) + u.Username = currUsername + u.NewUsername = fakeUsername + u.UID = fakeUID + u.GID = existingGID + u.Name = "test" + u.HomeDir = "/tmp/test" + u.MoveDir = true + u.Status = resource.NewStatus() + + expected := &user.ModUserOptions{ + Username: u.NewUsername, + UID: u.UID, + Group: u.GID, + Comment: u.Name, + Directory: u.HomeDir, + MoveDir: true, + } + + options, err := u.DiffMod(u.Status, currUser) + + assert.NoError(t, err) + assert.Equal(t, expected, options) + assert.Equal(t, resource.StatusWillChange, u.Status.StatusCode()) + assert.True(t, u.Status.HasChanges()) + assert.Equal(t, currUser.Username, u.Status.Diffs()["username"].Original()) + assert.Equal(t, u.NewUsername, u.Status.Diffs()["username"].Current()) + assert.Equal(t, currGID, u.Status.Diffs()["gid"].Original()) + assert.Equal(t, u.GID, u.Status.Diffs()["gid"].Current()) + assert.Equal(t, currUID, u.Status.Diffs()["uid"].Original()) + assert.Equal(t, u.UID, u.Status.Diffs()["uid"].Current()) + assert.Equal(t, currUser.Name, u.Status.Diffs()["comment"].Original()) + assert.Equal(t, u.Name, u.Status.Diffs()["comment"].Current()) + assert.Equal(t, currUser.HomeDir, u.Status.Diffs()["home_dir name"].Original()) + assert.Equal(t, u.HomeDir, u.Status.Diffs()["home_dir name"].Current()) + assert.Equal(t, currUser.HomeDir, u.Status.Diffs()["home_dir contents"].Original()) + assert.Equal(t, u.HomeDir, u.Status.Diffs()["home_dir contents"].Current()) + }) + + t.Run("username", func(t *testing.T) { + t.Run("user not found", func(t *testing.T) { + u := user.NewUser(new(user.System)) + u.Username = currUsername + u.NewUsername = fakeUsername + u.Status = resource.NewStatus() + + expected := &user.ModUserOptions{ + Username: u.NewUsername, + } + + options, err := u.DiffMod(u.Status, currUser) + + assert.NoError(t, err) + assert.Equal(t, expected, options) + assert.Equal(t, resource.StatusWillChange, u.Status.StatusCode()) + assert.True(t, u.Status.HasChanges()) + assert.Equal(t, currUser.Username, u.Status.Diffs()["username"].Original()) + assert.Equal(t, u.NewUsername, u.Status.Diffs()["username"].Current()) + }) + + t.Run("error-user found", func(t *testing.T) { + u := user.NewUser(new(user.System)) + u.Username = currUsername + u.NewUsername = existingUser.Username + u.Status = resource.NewStatus() + + options, err := u.DiffMod(u.Status, currUser) + + assert.EqualError(t, err, fmt.Sprintf("user %s already exists", u.NewUsername)) + assert.Nil(t, options) + assert.Equal(t, resource.StatusCantChange, u.Status.StatusCode()) + assert.True(t, u.Status.HasChanges()) + }) + }) + + t.Run("uid", func(t *testing.T) { + t.Run("uid not found", func(t *testing.T) { + u := user.NewUser(new(user.System)) + u.Username = currUsername + u.UID = fakeUID + u.Status = resource.NewStatus() + + expected := &user.ModUserOptions{ + UID: u.UID, + } - options, err := user.SetAddUserOptions(u) + options, err := u.DiffMod(u.Status, currUser) + + assert.NoError(t, err) + assert.Equal(t, expected, options) + assert.Equal(t, resource.StatusWillChange, u.Status.StatusCode()) + assert.True(t, u.Status.HasChanges()) + assert.Equal(t, currUID, u.Status.Diffs()["uid"].Original()) + assert.Equal(t, u.UID, u.Status.Diffs()["uid"].Current()) + }) + + t.Run("error-uid found", func(t *testing.T) { + u := user.NewUser(new(user.System)) + u.Username = currUsername + u.UID = existingUID + u.Status = resource.NewStatus() + + options, err := u.DiffMod(u.Status, currUser) + + assert.EqualError(t, err, fmt.Sprintf("uid %s already exists", u.UID)) + assert.Nil(t, options) + assert.Equal(t, resource.StatusCantChange, u.Status.StatusCode()) + assert.True(t, u.Status.HasChanges()) + }) + + t.Run("current uid-other options", func(t *testing.T) { + u := user.NewUser(new(user.System)) + u.Username = currUsername + u.UID = currUID + u.Name = "test" + u.Status = resource.NewStatus() + + expected := &user.ModUserOptions{ + Comment: "test", + } + + options, err := u.DiffMod(u.Status, currUser) + + assert.NoError(t, err) + assert.Equal(t, expected, options) + assert.Equal(t, resource.StatusWillChange, u.Status.StatusCode()) + assert.True(t, u.Status.HasChanges()) + assert.Equal(t, currUser.Name, u.Status.Diffs()["comment"].Original()) + assert.Equal(t, u.Name, u.Status.Diffs()["comment"].Current()) + assert.Equal(t, expected.UID, "") + }) + + t.Run("current uid-no other options", func(t *testing.T) { + u := user.NewUser(new(user.System)) + u.Username = currUsername + u.UID = currUID + u.Status = resource.NewStatus() + + expected := &user.ModUserOptions{} + + options, err := u.DiffMod(u.Status, currUser) + + assert.NoError(t, err) + assert.Equal(t, expected, options) + assert.Equal(t, resource.StatusNoChange, u.Status.StatusCode()) + assert.False(t, u.Status.HasChanges()) + }) + }) + + t.Run("group", func(t *testing.T) { + t.Run("with groupname", func(t *testing.T) { + u := user.NewUser(new(user.System)) + u.Username = currUsername + u.GroupName = existingGroupName + u.Status = resource.NewStatus() + + expected := &user.ModUserOptions{ + Group: u.GroupName, + } + + options, err := u.DiffMod(u.Status, currUser) + + assert.NoError(t, err) + assert.Equal(t, expected, options) + assert.Equal(t, resource.StatusWillChange, u.Status.StatusCode()) + assert.True(t, u.Status.HasChanges()) + assert.Equal(t, currGroupName, u.Status.Diffs()["group"].Original()) + assert.Equal(t, u.GroupName, u.Status.Diffs()["group"].Current()) + }) + + t.Run("error-groupname not found", func(t *testing.T) { + u := user.NewUser(new(user.System)) + u.Username = currUsername + u.GroupName = fakeGroupName + u.Status = resource.NewStatus() + + options, err := u.DiffMod(u.Status, currUser) + + assert.EqualError(t, err, fmt.Sprintf("group %s does not exist", u.GroupName)) + assert.Nil(t, options) + assert.Equal(t, resource.StatusCantChange, u.Status.StatusCode()) + assert.True(t, u.Status.HasChanges()) + }) + + t.Run("with gid", func(t *testing.T) { + u := user.NewUser(new(user.System)) + u.Username = currUsername + u.GID = existingGID + u.Status = resource.NewStatus() + + expected := &user.ModUserOptions{ + Group: u.GID, + } + + options, err := u.DiffMod(u.Status, currUser) + + assert.NoError(t, err) + assert.Equal(t, expected, options) + assert.Equal(t, resource.StatusWillChange, u.Status.StatusCode()) + assert.True(t, u.Status.HasChanges()) + assert.Equal(t, currGID, u.Status.Diffs()["gid"].Original()) + assert.Equal(t, u.GID, u.Status.Diffs()["gid"].Current()) + }) + + t.Run("error-gid not found", func(t *testing.T) { + u := user.NewUser(new(user.System)) + u.Username = currUsername + u.GID = fakeGID + u.Status = resource.NewStatus() + + options, err := u.DiffMod(u.Status, currUser) + + assert.EqualError(t, err, fmt.Sprintf("group gid %s does not exist", u.GID)) + assert.Nil(t, options) + assert.Equal(t, resource.StatusCantChange, u.Status.StatusCode()) + assert.True(t, u.Status.HasChanges()) + }) + + t.Run("user with groupname and gid", func(t *testing.T) { + u := user.NewUser(new(user.System)) + u.Username = currUsername + u.GroupName = existingGroupName + u.GID = existingGID + u.Status = resource.NewStatus() + + expected := &user.ModUserOptions{ + Group: u.GroupName, + } + + options, err := u.DiffMod(u.Status, currUser) + + assert.NoError(t, err) + assert.Equal(t, expected, options) + assert.Equal(t, resource.StatusWillChange, u.Status.StatusCode()) + assert.True(t, u.Status.HasChanges()) + assert.Equal(t, currGroupName, u.Status.Diffs()["group"].Original()) + assert.Equal(t, u.GroupName, u.Status.Diffs()["group"].Current()) + }) + }) + + t.Run("comment", func(t *testing.T) { + u := user.NewUser(new(user.System)) + u.Username = currUsername + u.Name = "test" + u.Status = resource.NewStatus() + + expected := &user.ModUserOptions{ + Comment: u.Name, + } + + options, err := u.DiffMod(u.Status, currUser) + + assert.NoError(t, err) + assert.Equal(t, expected, options) + assert.Equal(t, resource.StatusWillChange, u.Status.StatusCode()) + assert.True(t, u.Status.HasChanges()) + assert.Equal(t, currUser.Name, u.Status.Diffs()["comment"].Original()) + assert.Equal(t, u.Name, u.Status.Diffs()["comment"].Current()) + }) + + t.Run("directory", func(t *testing.T) { + u := user.NewUser(new(user.System)) + u.Username = currUsername + u.HomeDir = "/tmp/test" + u.Status = resource.NewStatus() + + expected := &user.ModUserOptions{ + Directory: u.HomeDir, + } + + options, err := u.DiffMod(u.Status, currUser) assert.NoError(t, err) - assert.Equal(t, "", options.UID) - assert.Equal(t, "", options.Group) - assert.Equal(t, "", options.Comment) - assert.Equal(t, "", options.Directory) + assert.Equal(t, expected, options) + assert.Equal(t, resource.StatusWillChange, u.Status.StatusCode()) + assert.True(t, u.Status.HasChanges()) + assert.Equal(t, currUser.HomeDir, u.Status.Diffs()["home_dir name"].Original()) + assert.Equal(t, u.HomeDir, u.Status.Diffs()["home_dir name"].Current()) + }) + + t.Run("no options", func(t *testing.T) { + u := user.NewUser(new(user.System)) + u.Username = currUsername + u.Status = resource.NewStatus() + + expected := &user.ModUserOptions{} + + options, err := u.DiffMod(u.Status, currUser) + + assert.NoError(t, err) + assert.Equal(t, expected, options) + assert.Equal(t, resource.StatusNoChange, u.Status.StatusCode()) + assert.False(t, u.Status.HasChanges()) }) } @@ -1019,12 +1333,13 @@ func setFakeUid() (string, error) { return "", fmt.Errorf("setFakeUid: could not set uid") } -// setGid is used to find a gid that exists. +// setGid is used to find a gid that exists, but is not +// the gid for the current user. func setGid() (string, error) { for i := 0; i <= maxGID; i++ { gid := strconv.Itoa(i) _, err := os.LookupGroupId(gid) - if err == nil { + if err == nil && gid != currGID { return gid, nil } } @@ -1043,17 +1358,23 @@ func setFakeGid() (string, error) { return "", fmt.Errorf("setFakeGid: could not set gid") } -// MockOptions is a mock implementation for setting user options -type MockOptions struct { +// MockDiff is a mock implementation for user diffs +type MockDiff struct { mock.Mock } -// SetAddUserOptions sets the options for adding a user -func (m *MockOptions) SetAddUserOptions(u *user.User) (*user.AddUserOptions, error) { - args := m.Called(u) +// DiffAdd sets the diffs and options for adding a user +func (m *MockDiff) DiffAdd(r resource.Status) (*user.AddUserOptions, error) { + args := m.Called(r) return args.Get(0).(*user.AddUserOptions), args.Error(1) } +// DiffMod sets the diffs and options for modifying a user +func (m *MockDiff) DiffMod(r resource.Status, u *user.User) (*user.ModUserOptions, error) { + args := m.Called(r, u) + return args.Get(0).(*user.ModUserOptions), args.Error(1) +} + // MockSystem is a mock implementation of user.System type MockSystem struct { mock.Mock @@ -1071,6 +1392,12 @@ func (m *MockSystem) DelUser(name string) error { return args.Error(0) } +// ModUser modifies a user +func (m *MockSystem) ModUser(name string, options *user.ModUserOptions) error { + args := m.Called(name, options) + return args.Error(0) +} + // Lookup looks up a user by name func (m *MockSystem) Lookup(name string) (*os.User, error) { args := m.Called(name)