jx/internal/resource/user.go

506 lines
13 KiB
Go
Raw Normal View History

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-05-06 00:48:54 +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-05-06 00:48:54 +00:00
"encoding/json"
"errors"
"gitea.rosskeen.house/rosskeen.house/machine"
2024-07-17 08:34:57 +00:00
"strings"
"decl/internal/codec"
2024-07-17 08:34:57 +00:00
"decl/internal/command"
"decl/internal/data"
"decl/internal/folio"
2024-05-06 00:48:54 +00:00
)
type decodeUser User
type UserType string
const (
UserTypeName TypeName = "user"
UserTypeAddUser UserType = "adduser"
UserTypeUserAdd UserType = "useradd"
2024-03-20 16:15:27 +00:00
)
2024-07-17 08:34:57 +00:00
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()
2024-03-20 16:15:27 +00:00
type User struct {
*Common `json:",inline" yaml:",inline"`
2024-05-09 07:39:45 +00:00
stater machine.Stater `json:"-" yaml:"-"`
Name string `json:"name" yaml:"name"`
2024-05-06 00:48:54 +00:00
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"`
2024-05-06 00:48:54 +00:00
UserType UserType `json:"-" yaml:"-"`
2024-03-25 20:31:06 +00:00
2024-07-17 08:34:57 +00:00
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:"-"`
2024-03-20 16:15:27 +00:00
}
func NewUser() *User {
return &User{ CreateHome: true, Common: &Common{ resourceType: UserTypeName } }
2024-03-20 16:15:27 +00:00
}
2024-03-22 04:35:17 +00:00
func init() {
folio.DocumentRegistry.ResourceTypes.Register([]string{"user"}, func(u *url.URL) data.Resource {
2024-03-25 20:31:06 +00:00
user := NewUser()
2024-05-06 00:48:54 +00:00
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()
2024-03-25 20:31:06 +00:00
return user
})
2024-03-22 04:35:17 +00:00
}
2024-07-17 08:34:57 +00:00
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) {
2024-07-17 08:34:57 +00:00
u.Resources = resources
}
func (u *User) Clone() data.Resource {
2024-05-06 00:48:54 +00:00
newu := &User {
Common: u.Common,
2024-04-19 07:52:10 +00:00
Name: u.Name,
UID: u.UID,
Group: u.Group,
Groups: u.Groups,
Gecos: u.Gecos,
Home: u.Home,
CreateHome: u.CreateHome,
Shell: u.Shell,
2024-05-06 00:48:54 +00:00
UserType: u.UserType,
2024-04-19 07:52:10 +00:00
}
2024-05-06 00:48:54 +00:00
newu.CreateCommand, newu.ReadCommand, newu.UpdateCommand, newu.DeleteCommand = u.UserType.NewCRUD()
return newu
}
func (u *User) StateMachine() machine.Stater {
2024-05-09 07:39:45 +00:00
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 {
2024-07-17 08:34:57 +00:00
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"
2024-07-17 08:34:57 +00:00
panic(triggerErr)
}
} else {
u.Common.State = "absent"
2024-07-17 08:34:57 +00:00
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"
2024-07-17 08:34:57 +00:00
panic(triggerErr)
}
} else {
u.Common.State = "present"
2024-07-17 08:34:57 +00:00
panic(deleteErr)
}
2024-05-09 07:39:45 +00:00
case "start_create":
2024-05-14 19:53:42 +00:00
if e := u.Create(ctx); e == nil {
if triggerErr := u.stater.Trigger("created"); triggerErr == nil {
return
2024-05-13 05:41:12 +00:00
}
2024-05-09 07:39:45 +00:00
}
u.Common.State = "absent"
2024-07-17 08:34:57 +00:00
case "absent":
u.Common.State = "absent"
2024-07-17 08:34:57 +00:00
case "present", "created", "read":
u.Common.State = "present"
2024-05-09 07:39:45 +00:00
}
case machine.EXITSTATEEVENT:
}
2024-04-19 07:52:10 +00:00
}
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) UseConfig(config data.ConfigurationValueGetter) {
u.config = config
}
2024-03-20 19:25:25 +00:00
func (u *User) ResolveId(ctx context.Context) string {
if u.config != nil {
if configUser, configUserErr := u.config.GetValue("user"); configUserErr == nil && u.Name == "" {
u.Name = configUser.(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-07-17 08:34:57 +00:00
ctx := context.Background()
switch u.Common.State {
2024-03-25 20:31:06 +00:00
case "present":
_, NoUserExists := LookupUID(u.Name)
if NoUserExists != nil {
2024-05-06 00:48:54 +00:00
cmdErr := u.Create(context.Background())
2024-03-25 20:31:06 +00:00
return cmdErr
}
case "absent":
2024-07-17 08:34:57 +00:00
cmdErr := u.Delete(ctx)
2024-03-25 20:31:06 +00:00
return cmdErr
}
return nil
2024-03-20 16:15:27 +00:00
}
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
2024-04-05 17:22:17 +00:00
}
func (u *User) LoadDecl(yamlResourceDeclaration string) error {
return u.LoadString(yamlResourceDeclaration, codec.FormatYaml)
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
2024-05-06 00:48:54 +00:00
func (u *User) Create(ctx context.Context) (error) {
_, err := u.CreateCommand.Execute(u)
if err != nil {
return err
}
_,e := u.Read(ctx)
return e
}
2024-03-22 04:35:17 +00:00
func (u *User) Read(ctx context.Context) ([]byte, error) {
2024-05-06 00:48:54 +00:00
exErr := u.ReadCommand.Extractor(nil, u)
if exErr != nil {
u.Common.State = "absent"
2024-03-25 20:31:06 +00:00
}
2024-05-06 00:48:54 +00:00
if yaml, yamlErr := yaml.Marshal(u); yamlErr != nil {
return yaml, yamlErr
} else {
return yaml, exErr
2024-03-25 20:31:06 +00:00
}
2024-05-06 00:48:54 +00:00
}
2024-03-25 20:31:06 +00:00
func (u *User) Update(ctx context.Context) (error) {
return u.Create(ctx)
}
2024-07-17 08:34:57 +00:00
func (u *User) Delete(ctx context.Context) (error) {
2024-05-06 00:48:54 +00:00
_, err := u.DeleteCommand.Execute(u)
if err != nil {
return err
2024-03-25 20:31:06 +00:00
}
2024-05-06 00:48:54 +00:00
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
}
2024-07-17 08:34:57 +00:00
func (u *UserType) NewCRUD() (create *command.Command, read *command.Command, update *command.Command, del *command.Command) {
2024-05-06 00:48:54 +00:00
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()
}
}
2024-07-17 08:34:57 +00:00
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()
}
2024-05-06 00:48:54 +00:00
func (u *UserType) UnmarshalValue(value string) error {
switch value {
case string(UserTypeUserAdd), string(UserTypeAddUser):
*u = UserType(value)
return nil
default:
2024-07-17 08:34:57 +00:00
return ErrInvalidUserType
2024-03-25 20:31:06 +00:00
}
2024-05-06 00:48:54 +00:00
}
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)
}
2024-07-17 08:34:57 +00:00
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"
2024-07-17 08:34:57 +00:00
u.UserType = SystemUserType
lineIndex++
}
return nil
}
return c
}
func NewUserAddCreateCommand() *command.Command {
c := command.NewCommand()
2024-05-06 00:48:54 +00:00
c.Path = "useradd"
2024-07-17 08:34:57 +00:00
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 }}"),
2024-05-06 00:48:54 +00:00
}
c.Extractor = func(out []byte, target any) error {
return nil
2024-05-06 04:40:34 +00:00
/*
2024-05-06 00:48:54 +00:00
for _,line := range strings.Split(string(out), "\n") {
if line == "iptables: Chain already exists." {
return nil
}
}
return fmt.Errorf(string(out))
2024-05-06 04:40:34 +00:00
*/
2024-05-06 00:48:54 +00:00
}
return c
}
2024-07-17 08:34:57 +00:00
func NewAddUserCreateCommand() *command.Command {
c := command.NewCommand()
2024-05-06 00:48:54 +00:00
c.Path = "adduser"
2024-07-17 08:34:57 +00:00
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 }}"),
2024-05-06 00:48:54 +00:00
}
c.Extractor = func(out []byte, target any) error {
return nil
2024-05-06 04:40:34 +00:00
/*
2024-05-06 00:48:54 +00:00
for _,line := range strings.Split(string(out), "\n") {
if line == "iptables: Chain already exists." {
return nil
}
}
return fmt.Errorf(string(out))
2024-05-06 04:40:34 +00:00
*/
2024-05-06 00:48:54 +00:00
}
return c
}
2024-03-25 20:31:06 +00:00
2024-07-17 08:34:57 +00:00
func NewUserReadCommand() *command.Command {
c := command.NewCommand()
2024-05-06 00:48:54 +00:00
c.Extractor = func(out []byte, target any) error {
u := target.(*User)
u.Common.State = "absent"
2024-05-06 00:48:54 +00:00
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"
2024-05-06 00:48:54 +00:00
}
}
return e
}
return c
}
2024-07-17 08:34:57 +00:00
func NewUserUpdateCommand() *command.Command {
2024-05-06 00:48:54 +00:00
return nil
}
2024-07-17 08:34:57 +00:00
func NewUserDelDeleteCommand() *command.Command {
c := command.NewCommand()
2024-05-06 00:48:54 +00:00
c.Path = "userdel"
2024-07-17 08:34:57 +00:00
c.Args = []command.CommandArg{
command.CommandArg("{{ .Name }}"),
2024-05-06 00:48:54 +00:00
}
c.Extractor = func(out []byte, target any) error {
return nil
}
return c
}
2024-07-17 08:34:57 +00:00
func NewDelUserDeleteCommand() *command.Command {
c := command.NewCommand()
2024-05-06 00:48:54 +00:00
c.Path = "deluser"
2024-07-17 08:34:57 +00:00
c.Args = []command.CommandArg{
command.CommandArg("{{ .Name }}"),
2024-05-06 00:48:54 +00:00
}
c.Extractor = func(out []byte, target any) error {
return nil
}
return c
2024-03-22 04:35:17 +00:00
}