Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/user expiry #581

Merged
merged 5 commits into from
Jan 9, 2017
Merged
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
8 changes: 8 additions & 0 deletions resource/user/preparer.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ package user
import (
"fmt"
"math"
"time"

"github.com/asteris-llc/converge/load/registry"
"github.com/asteris-llc/converge/resource"
Expand Down Expand Up @@ -72,6 +73,12 @@ type Preparer struct {
// HomeDir must also be indicated if MoveDir is set to true.
MoveDir bool `hcl:"move_dir"`

// Expiry is the date on which the user account will be disabled. The date is
// specified in the format YYYY-MM-DD. If not specified, the default expiry
// date specified by the EXPIRE variable in /etc/default/useradd, or an empty
// string (no expiry) will be used by default.
Expiry time.Time `hcl:"expiry"`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be *time.Time? Otherwise any other changes to a user will have the side effect of setting an expiry since we'll always be setting it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I only set the expiry if it is specified in the hcl. There are checks in user.go (DiffAdd and DiffMod) for this.


// State is whether the user should be present.
// The default value is present.
State State `hcl:"state" valid_values:"present,absent"`
Expand Down Expand Up @@ -111,6 +118,7 @@ func (p *Preparer) Prepare(ctx context.Context, render resource.Renderer) (resou
usr.HomeDir = p.HomeDir
usr.MoveDir = p.MoveDir
usr.State = p.State
usr.Expiry = p.Expiry

if p.UID != nil {
usr.UID = fmt.Sprintf("%v", *p.UID)
Expand Down
13 changes: 13 additions & 0 deletions resource/user/preparer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,13 @@ import (
"fmt"
"math"
"testing"
"time"

"github.com/asteris-llc/converge/helpers/fakerenderer"
"github.com/asteris-llc/converge/resource"
"github.com/asteris-llc/converge/resource/user"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/net/context"
)

Expand Down Expand Up @@ -141,6 +143,17 @@ func TestPrepare(t *testing.T) {

assert.NoError(t, err)
})

t.Run("expiry", func(t *testing.T) {
zone := time.FixedZone(time.Now().In(time.Local).Zone())
expiry, err := time.ParseInLocation(user.ShortForm, "1996-12-12", zone)
require.NoError(t, err)

p := user.Preparer{Username: "test", Expiry: expiry}
_, err = p.Prepare(context.Background(), &fr)

assert.NoError(t, err)
})
})

t.Run("invalid", func(t *testing.T) {
Expand Down
42 changes: 34 additions & 8 deletions resource/user/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ package user
import (
"fmt"
"os/user"
"time"

"github.com/asteris-llc/converge/resource"
"github.com/pkg/errors"
Expand All @@ -32,6 +33,12 @@ const (

// StateAbsent indicates the user should be absent
StateAbsent State = "absent"

// ShortForm layout for time parsing
ShortForm = "2006-01-02"

// MaxTime is the max representable time
MaxTime = "2038-01-19"
)

// User manages user users
Expand Down Expand Up @@ -67,6 +74,9 @@ type User struct {
// if the contents of the home directory should be moved
MoveDir bool `export:"movedir"`

// the date the user account will be disabled
Expiry time.Time `export:"expiry"`

// configured the user state
State State `export:"state"`

Expand All @@ -82,6 +92,7 @@ type AddUserOptions struct {
CreateHome bool
SkelDir string
Directory string
Expiry string
}

// ModUserOptions are the options specified in the configuration to be used
Expand All @@ -93,13 +104,15 @@ type ModUserOptions struct {
Comment string
Directory string
MoveDir bool
Expiry string
}

// 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
LookupUserExpiry(userName string) (time.Time, error)
Lookup(userName string) (*user.User, error)
LookupID(userID string) (*user.User, error)
LookupGroup(groupName string) (*user.Group, error)
Expand Down Expand Up @@ -303,14 +316,6 @@ func (u *User) Apply(context.Context) (resource.TaskStatus, error) {
return status, nil
}

func noOptionsSet(u *User) bool {
switch {
case u.UID != "", u.GroupName != "", u.GID != "", u.Name != "", u.HomeDir != "":
return false
}
return true
}

// 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.
Expand Down Expand Up @@ -384,6 +389,11 @@ func (u *User) DiffAdd(status *resource.Status) (*AddUserOptions, error) {
status.AddDifference("home_dir name", "<default home>", u.HomeDir, "")
}

if u.Expiry != (time.Time{}) {
options.Expiry = u.Expiry.Format(ShortForm)
status.AddDifference("expiry", "<default expiry>", options.Expiry, "")
}

if resource.AnyChanges(status.Differences) {
status.RaiseLevel(resource.StatusWillChange)
}
Expand Down Expand Up @@ -465,6 +475,22 @@ func (u *User) DiffMod(status *resource.Status, currUser *user.User) (*ModUserOp
}
}

if u.Expiry != (time.Time{}) {
expiry, err := u.system.LookupUserExpiry(u.Username)
if err != nil {
return nil, fmt.Errorf("could not acquire current expiry for %s: %s", u.Username, err)
}
currentExpiry := expiry.Format(ShortForm)
newExpiry := u.Expiry.Format(ShortForm)
if currentExpiry != newExpiry {
if currentExpiry == MaxTime {
currentExpiry = "never"
}
options.Expiry = newExpiry
status.AddDifference("expiry", currentExpiry, options.Expiry, "")
}
}

if resource.AnyChanges(status.Differences) {
status.RaiseLevel(resource.StatusWillChange)
}
Expand Down
6 changes: 6 additions & 0 deletions resource/user/user_default.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package user

import (
"os/user"
"time"
)

// System implements SystemUtils
Expand All @@ -38,6 +39,11 @@ func (s *System) ModUser(userName string, options *ModUserOptions) error {
return ErrUnsupported
}

// LookupUserExpiry implementation for systems which are not supported
func (s *System) LookupUserExpiry(userName string) (time.Time, error) {
return time.Time{}, ErrUnsupported
}

// Lookup implementation for systems which are not supported
func (s *System) Lookup(userName string) (*user.User, error) {
return nil, ErrUnsupported
Expand Down
60 changes: 56 additions & 4 deletions resource/user/user_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,12 @@
package user

import (
"fmt"
"bytes"
"github.com/pkg/errors"
"os/exec"
"os/user"
"strings"
"time"
)

// System implements SystemUtils
Expand All @@ -46,11 +49,14 @@ func (s *System) AddUser(userName string, options *AddUserOptions) error {
if options.Directory != "" {
args = append(args, "-d", options.Directory)
}
if options.Expiry != "" {
args = append(args, "-e", options.Expiry)
}

cmd := exec.Command("useradd", args...)
err := cmd.Run()
if err != nil {
return fmt.Errorf("useradd: %s", err)
return errors.Wrap(err, "useradd")
}
return nil
}
Expand All @@ -60,7 +66,7 @@ func (s *System) DelUser(userName string) error {
cmd := exec.Command("userdel", userName)
err := cmd.Run()
if err != nil {
return fmt.Errorf("userdel: %s", err)
return errors.Wrap(err, "userdel")
}
return nil
}
Expand All @@ -86,15 +92,38 @@ func (s *System) ModUser(userName string, options *ModUserOptions) error {
args = append(args, "-m")
}
}
if options.Expiry != "" {
args = append(args, "-e", options.Expiry)
}

cmd := exec.Command("usermod", args...)
err := cmd.Run()
if err != nil {
return fmt.Errorf("usermod: %s", err)
return errors.Wrap(err, "usermod")
}
return nil
}

// LookupUserExpiry looks up a user's expiry
func (s *System) LookupUserExpiry(userName string) (time.Time, error) {
var out bytes.Buffer

args := []string{"-l", userName}
cmd := exec.Command("chage", args...)
cmd.Stdout = &out
err := cmd.Run()
if err != nil {
return time.Time{}, errors.Wrap(err, "chage")
}

expiry, err := parseForExpiry(out.String())
if err != nil {
return time.Time{}, err
}

return expiry, 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) {
Expand All @@ -118,3 +147,26 @@ func (s *System) LookupGroup(groupName string) (*user.Group, error) {
func (s *System) LookupGroupID(groupID string) (*user.Group, error) {
return user.LookupGroupId(groupID)
}

// parseForExpiry takes a string and extracts the account expiration date and
// converts it to a time.Time. This function is specifically written to handle
// the output from the `chage -l <username>` command.
func parseForExpiry(data string) (time.Time, error) {
split := strings.Split(data, "\n")

for _, line := range split {
if strings.Contains(line, "Account expires") {
newsplit := strings.Split(line, ":")
rawExpiry := strings.Trim(newsplit[1], " ")
zone := time.FixedZone(time.Now().In(time.Local).Zone())

if rawExpiry == "never" {
// set current user time to max time
return time.ParseInLocation(ShortForm, MaxTime, zone)
}
return time.ParseInLocation("Jan 2, 2006", strings.Trim(newsplit[1], " "), zone)
}
}

return time.Time{}, errors.New("could not parse expiry data for current user")
}
Loading