2024-03-20 19:25:25 +00:00
|
|
|
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
|
2024-03-22 17:39:06 +00:00
|
|
|
|
2024-03-20 16:15:27 +00:00
|
|
|
package resource
|
|
|
|
|
|
|
|
import (
|
2024-03-25 20:31:06 +00:00
|
|
|
"context"
|
|
|
|
"fmt"
|
|
|
|
"gopkg.in/yaml.v3"
|
2024-04-05 17:22:17 +00:00
|
|
|
"log/slog"
|
2024-03-25 20:31:06 +00:00
|
|
|
"net/url"
|
2024-04-05 17:22:17 +00:00
|
|
|
_ "os"
|
2024-03-25 20:31:06 +00:00
|
|
|
"os/exec"
|
|
|
|
"os/user"
|
2024-04-05 17:22:17 +00:00
|
|
|
"io"
|
2024-03-25 20:31:06 +00:00
|
|
|
"strconv"
|
|
|
|
"strings"
|
2024-03-20 16:15:27 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
type User struct {
|
2024-04-03 16:54:50 +00:00
|
|
|
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"`
|
2024-03-25 20:31:06 +00:00
|
|
|
|
2024-04-03 16:54:50 +00:00
|
|
|
State string `json:"state" yaml:"state"`
|
2024-03-20 16:15:27 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func NewUser() *User {
|
2024-04-05 17:22:17 +00:00
|
|
|
return &User{}
|
2024-03-20 16:15:27 +00:00
|
|
|
}
|
|
|
|
|
2024-03-22 04:35:17 +00:00
|
|
|
func init() {
|
2024-03-25 20:31:06 +00:00
|
|
|
ResourceTypes.Register("user", func(u *url.URL) Resource {
|
|
|
|
user := NewUser()
|
|
|
|
user.Name = u.Path
|
|
|
|
user.UID, _ = LookupUID(u.Path)
|
|
|
|
return user
|
|
|
|
})
|
2024-03-22 04:35:17 +00:00
|
|
|
}
|
|
|
|
|
2024-04-19 07:52:10 +00:00
|
|
|
func (u *User) Clone() Resource {
|
|
|
|
return &User {
|
|
|
|
Name: u.Name,
|
|
|
|
UID: u.UID,
|
|
|
|
Group: u.Group,
|
|
|
|
Groups: u.Groups,
|
|
|
|
Gecos: u.Gecos,
|
|
|
|
Home: u.Home,
|
|
|
|
CreateHome: u.CreateHome,
|
|
|
|
Shell: u.Shell,
|
|
|
|
State: u.State,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (u *User) SetURI(uri string) error {
|
|
|
|
resourceUri, e := url.Parse(uri)
|
|
|
|
if e == nil {
|
|
|
|
if resourceUri.Scheme == "user" {
|
|
|
|
u.Name = resourceUri.Hostname()
|
|
|
|
} else {
|
|
|
|
e = fmt.Errorf("%w: %s is not a user", ErrInvalidResourceURI, uri)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return e
|
|
|
|
}
|
|
|
|
|
2024-03-20 19:25:25 +00:00
|
|
|
func (u *User) URI() string {
|
2024-03-25 20:31:06 +00:00
|
|
|
return fmt.Sprintf("user://%s", u.Name)
|
2024-03-20 19:25:25 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (u *User) ResolveId(ctx context.Context) string {
|
2024-03-25 20:31:06 +00:00
|
|
|
return LookupUIDString(u.Name)
|
2024-03-20 19:25:25 +00:00
|
|
|
}
|
|
|
|
|
2024-04-09 19:30:05 +00:00
|
|
|
func (u *User) Validate() error {
|
|
|
|
return fmt.Errorf("failed")
|
|
|
|
}
|
|
|
|
|
2024-03-20 16:15:27 +00:00
|
|
|
func (u *User) Apply() error {
|
2024-03-25 20:31:06 +00:00
|
|
|
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"
|
2024-04-03 19:27:16 +00:00
|
|
|
if addUserCommandErr := u.AddUserCommand(&args); addUserCommandErr != nil {
|
|
|
|
return addUserCommandErr
|
|
|
|
}
|
2024-03-25 20:31:06 +00:00
|
|
|
}
|
|
|
|
} else {
|
2024-04-03 19:27:16 +00:00
|
|
|
if userAddCommandErr := u.UserAddCommand(&args); userAddCommandErr != nil {
|
|
|
|
return userAddCommandErr
|
|
|
|
}
|
2024-03-25 20:31:06 +00:00
|
|
|
}
|
|
|
|
args = append(args, u.Name)
|
|
|
|
cmd := exec.Command(userCommandName, args...)
|
|
|
|
cmdOutput, cmdErr := cmd.CombinedOutput()
|
2024-04-05 17:22:17 +00:00
|
|
|
slog.Info("user command", "command", cmd.String(), "output", string(cmdOutput))
|
2024-03-25 20:31:06 +00:00
|
|
|
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()
|
2024-04-05 17:22:17 +00:00
|
|
|
slog.Info("user command", "command", cmd.String(), "output", string(cmdOutput))
|
2024-03-25 20:31:06 +00:00
|
|
|
return cmdErr
|
|
|
|
}
|
|
|
|
return nil
|
2024-03-20 16:15:27 +00:00
|
|
|
}
|
|
|
|
|
2024-04-05 17:22:17 +00:00
|
|
|
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)
|
2024-03-20 16:15:27 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (u *User) AddUserCommand(args *[]string) error {
|
2024-03-25 20:31:06 +00:00
|
|
|
*args = append(*args, "-D")
|
|
|
|
if u.Group != "" {
|
|
|
|
*args = append(*args, "-G", u.Group)
|
|
|
|
}
|
|
|
|
if u.Home != "" {
|
|
|
|
*args = append(*args, "-h", u.Home)
|
|
|
|
}
|
|
|
|
return nil
|
2024-03-20 16:15:27 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (u *User) UserAddCommand(args *[]string) error {
|
2024-03-25 20:31:06 +00:00
|
|
|
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
|
2024-03-20 16:15:27 +00:00
|
|
|
}
|
2024-03-20 19:25:25 +00:00
|
|
|
|
|
|
|
func (u *User) Type() string { return "user" }
|
2024-03-22 04:35:17 +00:00
|
|
|
|
|
|
|
func (u *User) Read(ctx context.Context) ([]byte, error) {
|
2024-03-25 20:31:06 +00:00
|
|
|
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)
|
2024-03-22 04:35:17 +00:00
|
|
|
}
|