// 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" "encoding/json" "errors" "gitea.rosskeen.house/rosskeen.house/machine" "strings" "decl/internal/codec" "decl/internal/command" "decl/internal/data" "decl/internal/folio" ) type decodeUser User type UserType string const ( UserTypeName TypeName = "user" UserTypeAddUser UserType = "adduser" UserTypeUserAdd UserType = "useradd" ) var ErrUnsupportedUserType error = errors.New("The UserType is not supported on this system") var ErrInvalidUserType error = errors.New("invalid UserType value") var SystemUserType UserType = FindSystemUserType() type User struct { *Common `json:",inline" yaml:",inline"` 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.Command `json:"-" yaml:"-"` ReadCommand *command.Command `json:"-" yaml:"-"` UpdateCommand *command.Command `json:"-" yaml:"-"` DeleteCommand *command.Command `json:"-" yaml:"-"` config data.ConfigurationValueGetter Resources data.ResourceMapper `json:"-" yaml:"-"` } func NewUser() *User { return &User{ CreateHome: true, Common: &Common{ resourceType: UserTypeName } } } func init() { folio.DocumentRegistry.ResourceTypes.Register([]string{"user"}, func(u *url.URL) data.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 FindSystemUserType() UserType { for _, userType := range []UserType{UserTypeAddUser, UserTypeUserAdd} { c := userType.NewCreateCommand() if c.Exists() { return userType } } return UserTypeAddUser } func (u *User) SetResourceMapper(resources data.ResourceMapper) { u.Resources = resources } func (u *User) Clone() data.Resource { newu := &User { Common: u.Common, Name: u.Name, UID: u.UID, Group: u.Group, Groups: u.Groups, Gecos: u.Gecos, Home: u.Home, CreateHome: u.CreateHome, Shell: u.Shell, 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_read": if _,readErr := u.Read(ctx); readErr == nil { if triggerErr := u.StateMachine().Trigger("state_read"); triggerErr == nil { return } else { u.Common.State = "absent" panic(triggerErr) } } else { u.Common.State = "absent" panic(readErr) } case "start_delete": if deleteErr := u.Delete(ctx); deleteErr == nil { if triggerErr := u.StateMachine().Trigger("deleted"); triggerErr == nil { return } else { u.Common.State = "present" panic(triggerErr) } } else { u.Common.State = "present" panic(deleteErr) } case "start_create": if e := u.Create(ctx); e == nil { if triggerErr := u.stater.Trigger("created"); triggerErr == nil { return } } u.Common.State = "absent" case "absent": u.Common.State = "absent" case "present", "created", "read": u.Common.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) UseConfig(config data.ConfigurationValueGetter) { u.config = config } 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 { ctx := context.Background() switch u.Common.State { case "present": _, NoUserExists := LookupUID(u.Name) if NoUserExists != nil { cmdErr := u.Create(context.Background()) return cmdErr } case "absent": cmdErr := u.Delete(ctx) return cmdErr } return nil } func (u *User) Load(docData []byte, f codec.Format) (err error) { err = f.StringDecoder(string(docData)).Decode(u) return } func (u *User) LoadReader(r io.ReadCloser, f codec.Format) (err error) { err = f.Decoder(r).Decode(u) return } func (u *User) LoadString(docData string, f codec.Format) (err error) { err = f.StringDecoder(docData).Decode(u) return } func (u *User) LoadDecl(yamlResourceDeclaration string) error { return u.LoadString(yamlResourceDeclaration, codec.FormatYaml) } 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.Common.State = "absent" } if yaml, yamlErr := yaml.Marshal(u); yamlErr != nil { return yaml, yamlErr } else { return yaml, exErr } } func (u *User) Update(ctx context.Context) (error) { return u.Create(ctx) } func (u *User) Delete(ctx context.Context) (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.Command, read *command.Command, update *command.Command, del *command.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) NewCreateCommand() (create *command.Command) { switch *u { case UserTypeUserAdd: return NewUserAddCreateCommand() case UserTypeAddUser: return NewAddUserCreateCommand() default: } return nil } func (u *UserType) NewReadCommand() (*command.Command) { return NewUserReadCommand() } func (p *UserType) NewReadUsersCommand() (*command.Command) { return NewReadUsersCommand() } func (u *UserType) UnmarshalValue(value string) error { switch value { case string(UserTypeUserAdd), string(UserTypeAddUser): *u = UserType(value) return nil default: return ErrInvalidUserType } } 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 NewReadUsersCommand() *command.Command { c := command.NewCommand() c.Path = "getent" c.Args = []command.CommandArg{ command.CommandArg("passwd"), } c.Extractor = func(out []byte, target any) error { Users := target.(*[]*User) lines := strings.Split(strings.TrimSpace(string(out)), "\n") lineIndex := 0 for _, line := range lines { userRecord := strings.Split(strings.TrimSpace(line), ":") if len(*Users) <= lineIndex + 1 { *Users = append(*Users, NewUser()) } u := (*Users)[lineIndex] u.Name = userRecord[0] u.UID = userRecord[2] u.Gecos = userRecord[4] u.Home = userRecord[5] u.Shell = userRecord[6] if readUser, userErr := user.Lookup(u.Name); userErr == nil { if groups, groupsErr := readUser.GroupIds(); groupsErr == nil { for _, secondaryGroup := range groups { if readGroup, groupErr := user.LookupGroupId(secondaryGroup); groupErr == nil { u.Groups = append(u.Groups, readGroup.Name) } } } } if readGroup, groupErr := user.LookupGroupId(userRecord[3]); groupErr == nil { u.Group = readGroup.Name } u.Common.State = "present" u.UserType = SystemUserType lineIndex++ } return nil } return c } func NewUserAddCreateCommand() *command.Command { c := command.NewCommand() c.Path = "useradd" c.Args = []command.CommandArg{ command.CommandArg("{{ if .UID }}-u {{ .UID }}{{ end }}"), command.CommandArg("{{ if .Gecos }}-c {{ .Gecos }}{{ end }}"), command.CommandArg("{{ if .Group }}-g {{ .Group }}{{ end }}"), command.CommandArg("{{ if .Groups }}-G {{ range .Groups }}{{ . }},{{- end }}{{ end }}"), command.CommandArg("{{ if .Home }}-d {{ .Home }}{{ end }}"), command.CommandArg("{{ if .CreateHome }}-m{{ else }}-M{{ end }}"), command.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.Command { c := command.NewCommand() c.Path = "adduser" c.Args = []command.CommandArg{ command.CommandArg("{{ if .UID }}-u {{ .UID }}{{ end }}"), command.CommandArg("{{ if .Gecos }}-g {{ .Gecos }}{{ end }}"), command.CommandArg("{{ if .Group }}-G {{ .Group }}{{ end }}"), command.CommandArg("{{ if .Home }}-h {{ .Home }}{{ end }}"), command.CommandArg("{{ if not .CreateHome }}-H{{ end }}"), command.CommandArg("-D"), command.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.Command { c := command.NewCommand() c.Extractor = func(out []byte, target any) error { u := target.(*User) u.Common.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.Common.State = "present" } } return e } return c } func NewUserUpdateCommand() *command.Command { return nil } func NewUserDelDeleteCommand() *command.Command { c := command.NewCommand() c.Path = "userdel" c.Args = []command.CommandArg{ command.CommandArg("{{ .Name }}"), } c.Extractor = func(out []byte, target any) error { return nil } return c } func NewDelUserDeleteCommand() *command.Command { c := command.NewCommand() c.Path = "deluser" c.Args = []command.CommandArg{ command.CommandArg("{{ .Name }}"), } c.Extractor = func(out []byte, target any) error { return nil } return c }