// 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" "strings" "encoding/json" "errors" "gitea.rosskeen.house/rosskeen.house/machine" "decl/internal/codec" ) type decodeUser User type UserType string const ( UserTypeAddUser = "adduser" UserTypeUserAdd = "useradd" ) type User struct { stater machine.Stater `json:"-" yaml:"-"` Name string `json:"name" yaml:"name"` UID string `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"` UserType UserType `json:"-" yaml:"-"` CreateCommand *Command `json:"-" yaml:"-"` ReadCommand *Command `json:"-" yaml:"-"` UpdateCommand *Command `json:"-" yaml:"-"` DeleteCommand *Command `json:"-" yaml:"-"` State string `json:"state,omitempty" yaml:"state,omitempty"` } func NewUser() *User { return &User{ CreateHome: true } } func init() { ResourceTypes.Register("user", func(u *url.URL) Resource { user := NewUser() user.Name = u.Hostname() user.UID = LookupUIDString(u.Hostname()) if _, addUserPathErr := exec.LookPath("adduser"); addUserPathErr == nil { user.UserType = UserTypeAddUser } if _, pathErr := exec.LookPath("useradd"); pathErr == nil { user.UserType = UserTypeUserAdd } user.CreateCommand, user.ReadCommand, user.UpdateCommand, user.DeleteCommand = user.UserType.NewCRUD() return user }) } func (u *User) Clone() Resource { newu := &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, UserType: u.UserType, } newu.CreateCommand, newu.ReadCommand, newu.UpdateCommand, newu.DeleteCommand = u.UserType.NewCRUD() return newu } func (u *User) StateMachine() machine.Stater { if u.stater == nil { u.stater = StorageMachine(u) } return u.stater } func (u *User) Notify(m *machine.EventMessage) { ctx := context.Background() switch m.On { case machine.ENTERSTATEEVENT: switch m.Dest { case "start_create": if e := u.Create(ctx); e != nil { if triggerErr := u.stater.Trigger("created"); triggerErr != nil { // transition error } } case "present": u.State = "present" } case machine.EXITSTATEEVENT: } } 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 } 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) Validate() error { return fmt.Errorf("failed") } func (u *User) Apply() error { switch u.State { case "present": _, NoUserExists := LookupUID(u.Name) if NoUserExists != nil { cmdErr := u.Create(context.Background()) return cmdErr } case "absent": cmdErr := u.Delete() return cmdErr } return nil } func (u *User) Load(r io.Reader) error { return codec.NewYAMLDecoder(r).Decode(u) } func (u *User) LoadDecl(yamlResourceDeclaration string) error { return codec.NewYAMLStringDecoder(yamlResourceDeclaration).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) Create(ctx context.Context) (error) { _, err := u.CreateCommand.Execute(u) if err != nil { return err } _,e := u.Read(ctx) return e } func (u *User) Read(ctx context.Context) ([]byte, error) { exErr := u.ReadCommand.Extractor(nil, u) if exErr != nil { u.State = "absent" } if yaml, yamlErr := yaml.Marshal(u); yamlErr != nil { return yaml, yamlErr } else { return yaml, exErr } } func (u *User) Delete() (error) { _, err := u.DeleteCommand.Execute(u) if err != nil { return err } return err } func (u *User) UnmarshalJSON(data []byte) error { if unmarshalErr := json.Unmarshal(data, (*decodeUser)(u)); unmarshalErr != nil { return unmarshalErr } u.CreateCommand, u.ReadCommand, u.UpdateCommand, u.DeleteCommand = u.UserType.NewCRUD() return nil } func (u *User) UnmarshalYAML(value *yaml.Node) error { if unmarshalErr := value.Decode((*decodeUser)(u)); unmarshalErr != nil { return unmarshalErr } u.CreateCommand, u.ReadCommand, u.UpdateCommand, u.DeleteCommand = u.UserType.NewCRUD() return nil } func (u *UserType) NewCRUD() (create *Command, read *Command, update *Command, del *Command) { switch *u { case UserTypeUserAdd: return NewUserAddCreateCommand(), NewUserReadCommand(), NewUserUpdateCommand(), NewUserDelDeleteCommand() case UserTypeAddUser: return NewAddUserCreateCommand(), NewUserReadCommand(), NewUserUpdateCommand(), NewDelUserDeleteCommand() default: if _, addUserPathErr := exec.LookPath("adduser"); addUserPathErr == nil { *u = UserTypeAddUser return NewAddUserCreateCommand(), NewUserReadCommand(), NewUserUpdateCommand(), NewDelUserDeleteCommand() } if _, pathErr := exec.LookPath("useradd"); pathErr == nil { *u = UserTypeUserAdd return NewUserAddCreateCommand(), NewUserReadCommand(), NewUserUpdateCommand(), NewUserDelDeleteCommand() } return NewUserAddCreateCommand(), NewUserReadCommand(), NewUserUpdateCommand(), NewUserDelDeleteCommand() } } func (u *UserType) UnmarshalValue(value string) error { switch value { case string(UserTypeUserAdd), string(UserTypeAddUser): *u = UserType(value) return nil default: return errors.New("invalid UserType value") } } func (u *UserType) UnmarshalJSON(data []byte) error { var s string if unmarshalUserTypeErr := json.Unmarshal(data, &s); unmarshalUserTypeErr != nil { return unmarshalUserTypeErr } return u.UnmarshalValue(s) } func (u *UserType) UnmarshalYAML(value *yaml.Node) error { var s string if err := value.Decode(&s); err != nil { return err } return u.UnmarshalValue(s) } func NewUserAddCreateCommand() *Command { c := NewCommand() c.Path = "useradd" c.Args = []CommandArg{ CommandArg("{{ if .UID }}-u {{ .UID }}{{ end }}"), CommandArg("{{ if .Gecos }}-c {{ .Gecos }}{{ end }}"), CommandArg("{{ if .Group }}-g {{ .Group }}{{ end }}"), CommandArg("{{ if .Groups }}-G {{ range .Groups }}{{ . }},{{- end }}{{ end }}"), CommandArg("{{ if .Home }}-d {{ .Home }}{{ end }}"), CommandArg("{{ if .CreateHome }}-m{{ else }}-M{{ end }}"), CommandArg("{{ .Name }}"), } c.Extractor = func(out []byte, target any) error { return nil /* for _,line := range strings.Split(string(out), "\n") { if line == "iptables: Chain already exists." { return nil } } return fmt.Errorf(string(out)) */ } return c } func NewAddUserCreateCommand() *Command { c := NewCommand() c.Path = "adduser" c.Args = []CommandArg{ CommandArg("{{ if .UID }}-u {{ .UID }}{{ end }}"), CommandArg("{{ if .Gecos }}-g {{ .Gecos }}{{ end }}"), CommandArg("{{ if .Group }}-G {{ .Group }}{{ end }}"), CommandArg("{{ if .Home }}-h {{ .Home }}{{ end }}"), CommandArg("{{ if not .CreateHome }}-H{{ end }}"), CommandArg("-D"), CommandArg("{{ .Name }}"), } c.Extractor = func(out []byte, target any) error { return nil /* for _,line := range strings.Split(string(out), "\n") { if line == "iptables: Chain already exists." { return nil } } return fmt.Errorf(string(out)) */ } return c } func NewUserReadCommand() *Command { c := NewCommand() c.Extractor = func(out []byte, target any) error { u := target.(*User) u.State = "absent" var readUser *user.User var e error if u.Name != "" { readUser, e = user.Lookup(u.Name) } else { if u.UID != "" { readUser, e = user.LookupId(u.UID) } } if e == nil { u.Name = readUser.Username u.UID = readUser.Uid u.Home = readUser.HomeDir u.Gecos = readUser.Name if readGroup, groupErr := user.LookupGroupId(readUser.Gid); groupErr == nil { u.Group = readGroup.Name } else { return groupErr } if u.UID != "" { u.State = "present" } } return e } return c } func NewUserUpdateCommand() *Command { return nil } func NewUserDelDeleteCommand() *Command { c := NewCommand() c.Path = "userdel" c.Args = []CommandArg{ CommandArg("{{ .Name }}"), } c.Extractor = func(out []byte, target any) error { return nil } return c } func NewDelUserDeleteCommand() *Command { c := NewCommand() c.Path = "deluser" c.Args = []CommandArg{ CommandArg("{{ .Name }}"), } c.Extractor = func(out []byte, target any) error { return nil } return c }