// Copyright 2024 Matthew Rich . All rights reserved. package resource import ( "context" "fmt" "gopkg.in/yaml.v3" "log/slog" "net/url" _ "os" "os/exec" "os/user" "io" "strconv" "strings" ) type User struct { Name string `json:"name" yaml:"name"` UID int `json:"uid,omitempty" yaml:"uid,omitempty"` Group string `json:"group,omitempty" yaml:"group,omitempty"` Groups []string `json:"groups,omitempty" yaml:"groups,omitempty"` Gecos string `json:"gecos,omitempty" yaml:"gecos,omitempty"` Home string `json:"home" yaml:"home"` CreateHome bool `json:"createhome,omitempty" yaml:"createhome,omitempty"` Shell string `json:"shell,omitempty" yaml:"shell,omitempty"` State string `json:"state" yaml:"state"` } func NewUser() *User { return &User{} } func init() { ResourceTypes.Register("user", func(u *url.URL) Resource { user := NewUser() user.Name = u.Path user.UID, _ = LookupUID(u.Path) return user }) } func (u *User) URI() string { return fmt.Sprintf("user://%s", u.Name) } func (u *User) ResolveId(ctx context.Context) string { return LookupUIDString(u.Name) } func (u *User) Apply() error { switch u.State { case "present": _, NoUserExists := LookupUID(u.Name) if NoUserExists != nil { var userCommandName string = "useradd" args := make([]string, 0, 7) if u.UID >= 0 { args = append(args, "-u", fmt.Sprintf("%d", u.UID)) } if _, pathErr := exec.LookPath("useradd"); pathErr != nil { if _, addUserPathErr := exec.LookPath("adduser"); addUserPathErr == nil { userCommandName = "adduser" if addUserCommandErr := u.AddUserCommand(&args); addUserCommandErr != nil { return addUserCommandErr } } } else { if userAddCommandErr := u.UserAddCommand(&args); userAddCommandErr != nil { return userAddCommandErr } } args = append(args, u.Name) cmd := exec.Command(userCommandName, args...) cmdOutput, cmdErr := cmd.CombinedOutput() slog.Info("user command", "command", cmd.String(), "output", string(cmdOutput)) return cmdErr } case "absent": var userDelCommandName string = "userdel" args := make([]string, 0, 7) if _, pathErr := exec.LookPath("userdel"); pathErr != nil { if _, delUserPathErr := exec.LookPath("deluser"); delUserPathErr == nil { userDelCommandName = "deluser" } } args = append(args, u.Name) cmd := exec.Command(userDelCommandName, args...) cmdOutput, cmdErr := cmd.CombinedOutput() slog.Info("user command", "command", cmd.String(), "output", string(cmdOutput)) return cmdErr } return nil } func (u *User) Load(r io.Reader) error { c := NewYAMLDecoder(r) return c.Decode(u) } func (u *User) LoadDecl(yamlResourceDeclaration string) error { c := NewYAMLStringDecoder(yamlResourceDeclaration) return c.Decode(u) } func (u *User) AddUserCommand(args *[]string) error { *args = append(*args, "-D") if u.Group != "" { *args = append(*args, "-G", u.Group) } if u.Home != "" { *args = append(*args, "-h", u.Home) } return nil } func (u *User) UserAddCommand(args *[]string) error { if u.Group != "" { *args = append(*args, "-g", u.Group) } if len(u.Groups) > 0 { *args = append(*args, "-G", strings.Join(u.Groups, ",")) } if u.Home != "" { *args = append(*args, "-d", u.Home) } if u.CreateHome { *args = append(*args, "-m") } return nil } func (u *User) Type() string { return "user" } func (u *User) Read(ctx context.Context) ([]byte, error) { var readUser *user.User var e error if u.Name != "" { readUser, e = user.Lookup(u.Name) } if u.UID >= 0 { readUser, e = user.LookupId(strconv.Itoa(u.UID)) } if e != nil { panic(e) } u.Name = readUser.Username u.UID, _ = strconv.Atoi(readUser.Uid) if readGroup, groupErr := user.LookupGroupId(readUser.Gid); groupErr == nil { u.Group = readGroup.Name } else { panic(groupErr) } u.Home = readUser.HomeDir u.Gecos = readUser.Name return yaml.Marshal(u) }