diff --git a/examples/autoedges4.yaml b/examples/autoedges4.yaml new file mode 100644 index 00000000..48a9c1c9 --- /dev/null +++ b/examples/autoedges4.yaml @@ -0,0 +1,19 @@ +--- +graph: mygraph +resources: + user: + - name: edgeuser + state: absent + gid: 10000 + - name: edgeuser2 + state: exists + group: edgegroup + groups: [edgegroup2, edgegroup3] + group: + - name: edgegroup + state: exists + gid: 10000 + - name: edgegroup2 + state: exists + - name: edgegroup3 + state: exists diff --git a/resources/group.go b/resources/group.go index 55990674..40526994 100644 --- a/resources/group.go +++ b/resources/group.go @@ -41,7 +41,7 @@ const groupFile = "/etc/group" type GroupRes struct { BaseRes `yaml:",inline"` State string `yaml:"state"` // state: exists, absent - GID *uint32 `yaml:"gid"` + GID *uint32 `yaml:"gid"` // the group's gid recWatcher *recwatch.RecWatcher } @@ -130,7 +130,6 @@ func (obj *GroupRes) CheckApply(apply bool) (checkOK bool, err error) { if _, ok := err.(user.UnknownGroupError); !ok { return false, errwrap.Wrapf(err, "error looking up group") } - log.Printf("%s: Group not found: %s", obj, obj.GetName()) exists = false } // if the group doesn't exist and should be absent, we are done @@ -225,6 +224,26 @@ func (obj *GroupRes) CheckApply(apply bool) (checkOK bool, err error) { type GroupUID struct { BaseUID name string + gid *uint32 +} + +// IFF aka if and only if they are equivalent, return true. If not, false. +func (obj *GroupUID) IFF(uid ResUID) bool { + res, ok := uid.(*GroupUID) + if !ok { + return false + } + if obj.gid != nil && res.gid != nil { + if *obj.gid != *res.gid { + return false + } + } + if obj.name != "" && res.name != "" { + if obj.name != res.name { + return false + } + } + return true } // UIDs includes all params to make a unique identification of this object. @@ -233,6 +252,7 @@ func (obj *GroupRes) UIDs() []ResUID { x := &GroupUID{ BaseUID: BaseUID{Name: obj.GetName(), Kind: obj.GetKind()}, name: obj.Name, + gid: obj.GID, } return []ResUID{x} } diff --git a/resources/user.go b/resources/user.go index e1d80a82..4c95740f 100644 --- a/resources/user.go +++ b/resources/user.go @@ -19,10 +19,13 @@ package resources import ( "fmt" + "io/ioutil" "log" "os/exec" "os/user" + "sort" "strconv" + "strings" "syscall" "github.com/purpleidea/mgmt/recwatch" @@ -39,11 +42,13 @@ const passwdFile = "/etc/passwd" // UserRes is a user account resource. type UserRes struct { BaseRes `yaml:",inline"` - State string `yaml:"state"` // state: exists, absent - UID *uint32 `yaml:"uid"` - GID *uint32 `yaml:"gid"` - HomeDir *string `yaml:"homedir"` - AllowDuplicateUID bool `yaml:"allowduplicateuid"` + State string `yaml:"state"` // state: exists, absent + UID *uint32 `yaml:"uid"` // uid must be unique unless AllowDuplicateUID is true + GID *uint32 `yaml:"gid"` // gid of the user's primary group + Group *string `yaml:"group"` // name of the user's primary group + Groups []string `yaml:"groups"` // list of supplemental groups + HomeDir *string `yaml:"homedir"` // path to the user's home directory + AllowDuplicateUID bool `yaml:"allowduplicateuid"` // allow duplicate uid recWatcher *recwatch.RecWatcher } @@ -59,8 +64,35 @@ func (obj *UserRes) Default() Res { // Validate if the params passed in are valid data. func (obj *UserRes) Validate() error { + const whitelist string = "_abcdefghijklmnopqrstuvwxyz0123456789" + if obj.State != "exists" && obj.State != "absent" { - return fmt.Errorf("State must be 'exists' or 'absent'") + return fmt.Errorf("state must be 'exists' or 'absent'") + } + if obj.GID != nil && obj.Group != nil { + return fmt.Errorf("cannot use both GID and Group") + } + if obj.Group != nil { + if *obj.Group == "" { + return fmt.Errorf("group cannot be empty string") + } + for _, char := range *obj.Group { + if !strings.Contains(whitelist, string(char)) { + return fmt.Errorf("group contains invalid character(s)") + } + } + } + if obj.Groups != nil { + for _, group := range obj.Groups { + if group == "" { + return fmt.Errorf("group cannot be empty string") + } + for _, char := range group { + if !strings.Contains(whitelist, string(char)) { + return fmt.Errorf("groups list contains invalid character(s)") + } + } + } } return obj.BaseRes.Validate() } @@ -89,7 +121,7 @@ func (obj *UserRes) Watch() error { for { if obj.debug { - log.Printf("Watching: %s", passwdFile) // attempting to watch... + log.Printf("%s: Watching: %s", obj, passwdFile) // attempting to watch... } select { @@ -131,7 +163,6 @@ func (obj *UserRes) CheckApply(apply bool) (checkOK bool, err error) { if _, ok := err.(user.UnknownUserError); !ok { return false, errwrap.Wrapf(err, "error looking up user") } - log.Printf("the user: %s does not exist", obj.GetName()) exists = false } @@ -182,10 +213,10 @@ func (obj *UserRes) CheckApply(apply bool) (checkOK bool, err error) { if obj.State == "exists" { if exists { cmdName = "usermod" - log.Printf("modifying user: %s", obj.GetName()) + log.Printf("%s: Modifying user: %s", obj, obj.GetName()) } else { cmdName = "useradd" - log.Printf("adding user: %s", obj.GetName()) + log.Printf("%s: Adding user: %s", obj, obj.GetName()) } if obj.AllowDuplicateUID { args = append(args, "--non-unique") @@ -196,12 +227,19 @@ func (obj *UserRes) CheckApply(apply bool) (checkOK bool, err error) { if obj.GID != nil { args = append(args, "-g", fmt.Sprintf("%d", *obj.GID)) } + if obj.Group != nil { + args = append(args, "-g", *obj.Group) + } + if obj.Groups != nil { + args = append(args, "-G", strings.Join(obj.Groups, ",")) + } if obj.HomeDir != nil { args = append(args, "-d", *obj.HomeDir) } } if obj.State == "absent" { cmdName = "userdel" + log.Printf("%s: Deleting user: %s", obj, obj.GetName()) } args = append(args, obj.GetName()) @@ -211,8 +249,25 @@ func (obj *UserRes) CheckApply(apply bool) (checkOK bool, err error) { Setpgid: true, Pgid: 0, } - if err := cmd.Run(); err != nil { - return false, errwrap.Wrapf(err, "cmd failed to run") + + // open a pipe to get error messages from os/exec + stderr, err := cmd.StderrPipe() + if err != nil { + return false, errwrap.Wrapf(err, "failed to initialize stderr pipe") + } + + // start the command + if err := cmd.Start(); err != nil { + return false, errwrap.Wrapf(err, "cmd failed to start") + } + // capture any error messages + slurp, err := ioutil.ReadAll(stderr) + if err != nil { + return false, errwrap.Wrapf(err, "error slurping error message") + } + // wait until cmd exits and return error message if any + if err := cmd.Wait(); err != nil { + return false, errwrap.Wrapf(err, "%s", slurp) } return false, nil @@ -224,6 +279,75 @@ type UserUID struct { name string } +// UserResAutoEdges holds the state of the auto edge generator. +type UserResAutoEdges struct { + UIDs []ResUID + pointer int +} + +// AutoEdges returns edges from the user resource to each group found in +// its definition. The groups can be in any of the three applicable fields +// (GID, Group and Groups.) If the user exists, reversed ensures the edge +// goes from group to user, and if the user is absent the edge goes from +// user to group. This ensures that we don't add users to groups that +// don't exist or delete groups before we delete their members. +func (obj *UserRes) AutoEdges() (AutoEdge, error) { + var result []ResUID + var reversed bool + if obj.State == "exists" { + reversed = true + } + if obj.GID != nil { + result = append(result, &GroupUID{ + BaseUID: BaseUID{ + Reversed: &reversed, + }, + gid: obj.GID, + }) + } + if obj.Group != nil { + result = append(result, &GroupUID{ + BaseUID: BaseUID{ + Reversed: &reversed, + }, + name: *obj.Group, + }) + } + for _, group := range obj.Groups { + result = append(result, &GroupUID{ + BaseUID: BaseUID{ + Reversed: &reversed, + }, + name: group, + }) + } + return &UserResAutoEdges{ + UIDs: result, + pointer: 0, + }, nil +} + +// Next returns the next automatic edge. +func (obj *UserResAutoEdges) Next() []ResUID { + if len(obj.UIDs) == 0 { + return nil + } + value := obj.UIDs[obj.pointer] + obj.pointer++ + return []ResUID{value} +} + +// Test gets results of the earlier Next() call, & returns if we should continue. +func (obj *UserResAutoEdges) Test(input []bool) bool { + if len(obj.UIDs) <= obj.pointer { + return false + } + if len(input) != 1 { // in case we get given bad data + log.Fatal("Expecting a single value!") + } + return true // keep going +} + // UIDs includes all params to make a unique identification of this object. // Most resources only return one, although some resources can return multiple. func (obj *UserRes) UIDs() []ResUID { @@ -275,6 +399,23 @@ func (obj *UserRes) Compare(r Res) bool { return false } } + if (obj.Groups == nil) != (res.Groups == nil) { + return false + } + if obj.Groups != nil && res.Groups != nil { + if len(obj.Groups) != len(res.Groups) { + return false + } + objGroups := obj.Groups + resGroups := res.Groups + sort.Strings(objGroups) + sort.Strings(resGroups) + for i := range objGroups { + if objGroups[i] != resGroups[i] { + return false + } + } + } if (obj.HomeDir == nil) != (res.HomeDir == nil) { return false }