From 597d9edbedc3b414eae29f7e658beeff8aaf9312 Mon Sep 17 00:00:00 2001 From: Theo <tss24@inf.ufpr.br> Date: Wed, 23 Apr 2025 22:31:27 -0300 Subject: [PATCH] improve validation and refactor opts --- cmd/bulk.go | 2 +- cmd/user/create.go | 54 +++----- cmd/user/group.go | 3 +- cmd/user/mod.go | 5 +- cmd/user/remove.go | 30 +++-- cmd/user/temp.go | 3 +- cmd/user/unblock.go | 3 +- internal/auth/krb.go | 6 +- internal/auth/ldap.go | 51 ++++--- internal/entities/opts/opts.go | 163 +++++++++-------------- internal/manage/manage.go | 44 +++--- internal/validate/validate.go | 235 +++++++++++++++++++++++++++++---- 12 files changed, 363 insertions(+), 236 deletions(-) diff --git a/cmd/bulk.go b/cmd/bulk.go index 4b24061..effd2c4 100644 --- a/cmd/bulk.go +++ b/cmd/bulk.go @@ -110,7 +110,7 @@ func bulkCreate(cmd *cobra.Command, args []string) error { err := validate.GRR(users, u.GRR) if err != nil { - fmt.Printf("Entry %v: malformed GRR: %v\nSkipping...\n", i+1, u.GRR) + fmt.Printf("Entry %v: malformed GRR: %v\nSkipping...\n", i+1, err) continue } diff --git a/cmd/user/create.go b/cmd/user/create.go index 7ac5a6a..c26366a 100644 --- a/cmd/user/create.go +++ b/cmd/user/create.go @@ -19,8 +19,6 @@ var CreateCmd = &cobra.Command{ } func init() { - // possible flags - // FIXME: maybe leave less flags for user input CreateCmd.Flags().StringP("grr", "r", "_", "User GRR, required for ini type") CreateCmd.Flags().StringP("type", "t", "ini", "Type of auto-generated login: ini, first or last") CreateCmd.Flags().StringP("name", "n", "_", "User full name, required, use quotes for spaces") @@ -35,7 +33,6 @@ func init() { CreateCmd.Flags().StringP("nobkp", "b", "", "User nobackup directory path") CreateCmd.Flags().BoolP("web", "w", false, "Generate webdir") - // required flags CreateCmd.MarkFlagRequired("name") CreateCmd.MarkFlagRequired("group") @@ -79,7 +76,7 @@ func createUserCmd(cmd *cobra.Command, args []string) error { err = utils.ClearCache() if err != nil { - fmt.Printf("Failed to reload cache, changes might take a while\nError: %v\n", err) + fmt.Printf("Failed to reload cache, changes might take a while to take effect\nError: %v\n", err) } return nil @@ -95,10 +92,6 @@ func createNewUserModel(cmd *cobra.Command) (user.User, bool, error) { return u, false, err } - if err := validateInputs(opts); err != nil { - return u, false, err - } - if opts.Status == "Blocked" { opts.Shell = "/bin/false" } @@ -129,11 +122,14 @@ func createNewUserModel(cmd *cobra.Command) (user.User, bool, error) { return u, false, err } + if err := validateUser(u); err != nil { + return u, false, err + } + return u, opts.Confirm, nil } -func validateInputs(opts opts.Opts) error { - +func validateUser(u user.User) error { l, err := auth.ConnLDAP() if err != nil { return err @@ -150,33 +146,13 @@ func validateInputs(opts opts.Opts) error { return err } - err = validate.GID(groups, opts.GID) - if err != nil { - return err - } - - err = validate.GRR(users, opts.GRR) - if err != nil { - return err - } - - err = validate.Expiry(opts.Expiry) - if err != nil { - return err - } - - err = validate.Status(opts.Status) - if err != nil { - return err - } - - // it’s OK if UID is empty here, we generate it later :) - if opts.UID != "" { - err := validate.UID(users, opts.UID) - if err != nil { - return err - } - } - - return nil + return validate.All( + func() error { return validate.Status(u.Status) }, + func() error { return validate.Expiry(u.Expiry) }, + func() error { return validate.GRR(users, u.GRR) }, + func() error { return validate.LoginUnique(users, u.UID) }, + func() error { return validate.GroupUnique(groups, u.GID) }, + func() error { return validate.PathExists(u.Homedir, "homedir") }, + func() error { return validate.PathExists(u.Nobackup, "nobackup") }, + ) } diff --git a/cmd/user/group.go b/cmd/user/group.go index bff48de..af5aa34 100644 --- a/cmd/user/group.go +++ b/cmd/user/group.go @@ -12,7 +12,7 @@ import ( var GroupCmd = &cobra.Command{ Use: "group [username] [new-group]", Short: "Change a user's base group", - Example: " useradm user group temp2 prof", + Example: " useradm user group mvrp22 ppginf", Args: cobra.ExactArgs(2), RunE: groupUserCmd, } @@ -44,6 +44,7 @@ func groupUserCmd(cmd *cobra.Command, args []string) error { return err } + // checks if group does in fact exist _, err = utils.GetGIDNumFromGID(groups, args[1]) if err != nil { return err diff --git a/cmd/user/mod.go b/cmd/user/mod.go index 761579d..113f6fb 100644 --- a/cmd/user/mod.go +++ b/cmd/user/mod.go @@ -1,6 +1,5 @@ package user -// FIXME: mudanças não são mostradas kkkkkk import ( "fmt" "os" @@ -82,13 +81,13 @@ func modUserCmd(cmd *cobra.Command, args []string) error { changes, err := promptUserYaml(state) if changes.GRR != curr.GRR { - err = validate.GRR(users, changes.GRR) + err := validate.GRR(users, changes.GRR) if err != nil { return err } } - err = validate.GID(groups, changes.Group) + err = validate.GroupUnique(groups, changes.Group) if err != nil { return err } diff --git a/cmd/user/remove.go b/cmd/user/remove.go index 3be7c7e..2e8f863 100644 --- a/cmd/user/remove.go +++ b/cmd/user/remove.go @@ -9,6 +9,7 @@ import ( "gitlab.c3sl.ufpr.br/tss24/useradm/internal/auth" "gitlab.c3sl.ufpr.br/tss24/useradm/internal/entities/opts" "gitlab.c3sl.ufpr.br/tss24/useradm/internal/manage" + "gitlab.c3sl.ufpr.br/tss24/useradm/internal/validate" "gitlab.c3sl.ufpr.br/tss24/useradm/pkg/utils" ) @@ -59,17 +60,28 @@ func removeUserCmd(cmd *cobra.Command, args []string) error { utils.ConfirmationPrompt(opts.Confirm, "user removal") + err = validate.PathExists(filepath.Join(manage.HOME_TRASH, + filepath.Base(u.Homedir)), "homedir") + if err != nil { + return err + } + + err = validate.PathExists(filepath.Join(manage.NO_BKP_TRASH, + filepath.Base(u.Nobackup)), "nobackup") + if err != nil { + return err + } + + if u.Webdir != "_" { + err = validate.PathExists(filepath.Join(manage.WEB_TRASH, + filepath.Base(u.Webdir)), "webdir") + if err != nil { + return err + } + } + err = manage.MoveDirsToTrash(u) if err != nil { - // scuffed i know, in the edge case there is already a - // directory in trash with the username this will fail - // and the defer will activate. That moves the trash dir - // to the users home which is bad, so if a dir move fails, - // it's better not to try to fix it. Just warn the admin - // to do it by hand, either removing the destination dir - // or changing the name. Remeber that having no dir to move - // is not a fail, so you can run the command again no fear - success = true return err } diff --git a/cmd/user/temp.go b/cmd/user/temp.go index 101a2fd..a348c3e 100644 --- a/cmd/user/temp.go +++ b/cmd/user/temp.go @@ -1,5 +1,6 @@ package user +// Pretty bad tbh import ( "fmt" "os" @@ -66,7 +67,7 @@ func tempCreate(cmd *cobra.Command, args []string) error { } } - err = validate.GID(groups, opts.GID) + err = validate.GroupUnique(groups, opts.GID) if err != nil { return err } diff --git a/cmd/user/unblock.go b/cmd/user/unblock.go index 9d14694..795f38b 100644 --- a/cmd/user/unblock.go +++ b/cmd/user/unblock.go @@ -35,8 +35,7 @@ func unblockUserCmd(cmd *cobra.Command, args []string) error { } defer l.Close() - login := args[0] - u, err := manage.Locate(l, login) + u, err := manage.Locate(l, args[0]) if err != nil { return err } diff --git a/internal/auth/krb.go b/internal/auth/krb.go index 4c4c33c..68af391 100644 --- a/internal/auth/krb.go +++ b/internal/auth/krb.go @@ -48,7 +48,7 @@ func ModKRBPassword(login, password string) error { } if err != nil { - return fmt.Errorf("Command execution failed: %v\nOutput:\n%s", err, outStr) + return fmt.Errorf("Password change failed: %v\nOutput:\n%s", err, outStr) } return nil @@ -59,7 +59,7 @@ func DelKRBPrincipal(login string) error { output, err := cmd.CombinedOutput() if err != nil { - return fmt.Errorf("Fail to delete Kerberos principal: %v\nOutput: %s", err, output) + return fmt.Errorf("Failed to delete Kerberos principal: %v\nOutput: %s", err, output) } return nil @@ -70,7 +70,7 @@ func KRBRandKey(login string) error { output, err := cmd.CombinedOutput() if err != nil { - return fmt.Errorf("Fail to set randkey in Kerberos: %v\nOutput: %s", err, output) + return fmt.Errorf("Failed to set randkey in Kerberos: %v\nOutput: %s", err, output) } return nil diff --git a/internal/auth/ldap.go b/internal/auth/ldap.go index 2264bc3..3b19a91 100644 --- a/internal/auth/ldap.go +++ b/internal/auth/ldap.go @@ -14,10 +14,13 @@ import ( const ( PASSWD_PATH = "/etc/ldapscripts/ldapscripts.passwd" + STD_DC = "dc=c3local" + GROUP_REQ = "ou=grupos," + STD_DC MIN_GID = 1000 MAX_GID = 5000 MIN_UID = 1000 MAX_UID = 30000 + SEARCH_FAIL = "LDAP search failed: %w" ) // stablishes a connection to LDAP @@ -86,10 +89,10 @@ func AddUserToLDAP(l *ldap.Conn, u *user.User) error { func DelFromLDAP(l *ldap.Conn, UID string) error { // search for all groups the user is a member of searchReq := ldap.NewSearchRequest( - "ou=grupos,dc=c3local", + GROUP_REQ, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, - "(memberUid="+UID+")", // Filter by UID membership + "(memberUid="+UID+")", // filter by UID membership []string{"dn", "cn"}, nil, ) @@ -119,17 +122,14 @@ func DelFromLDAP(l *ldap.Conn, UID string) error { } func CreateGroupLDAP(l *ldap.Conn, GID, GIDNumber string) error { - // contruct dn - dn := fmt.Sprintf("cn=%s,ou=grupos,dc=c3local", GID) + dn := fmt.Sprintf("cn=%s,%s", GID, GROUP_REQ) req := ldap.NewAddRequest(dn, []ldap.Control{}) - // contruct request req.Attribute("objectClass", []string{"posixGroup"}) req.Attribute("cn", []string{GID}) req.Attribute("gidNumber", []string{GIDNumber}) - // create if err := l.Add(req); err != nil { return fmt.Errorf("failed to add group: %w", err) } @@ -138,12 +138,11 @@ func CreateGroupLDAP(l *ldap.Conn, GID, GIDNumber string) error { } func DeleteGroupLDAP(l *ldap.Conn, GID string) error { - // construct dn - dn := fmt.Sprintf("cn=%s,ou=grupos,dc=c3local", GID) + dn := fmt.Sprintf("cn=%s,%s", GID, GROUP_REQ) - delReq := ldap.NewDelRequest(dn, []ldap.Control{}) + req := ldap.NewDelRequest(dn, []ldap.Control{}) - if err := l.Del(delReq); err != nil { + if err := l.Del(req); err != nil { return fmt.Errorf("failed to delete group: %w", err) } @@ -151,26 +150,26 @@ func DeleteGroupLDAP(l *ldap.Conn, GID string) error { } func AddToGroupLDAP(l *ldap.Conn, UID, GID string) error { - groupDN := fmt.Sprintf("cn=%s,ou=grupos,dc=c3local", GID) - modReq := ldap.NewModifyRequest(groupDN, nil) - modReq.Add("memberUid", []string{UID}) + groupDN := fmt.Sprintf("cn=%s,%s", GID, GROUP_REQ) + req := ldap.NewModifyRequest(groupDN, nil) + req.Add("memberUid", []string{UID}) - err := l.Modify(modReq) + err := l.Modify(req) if err != nil { - return fmt.Errorf("Failed to add user %s to group %s: %v", UID, GID, err) + return fmt.Errorf("failed to add user %s to group %s: %w", UID, GID, err) } return nil } func DelFromGroupLDAP(l *ldap.Conn, userUID, GID string) error { - groupDN := fmt.Sprintf("cn=%s,ou=grupos,dc=c3local", GID) + groupDN := fmt.Sprintf("cn=%s,%s", GID, GROUP_REQ) modReq := ldap.NewModifyRequest(groupDN, nil) modReq.Delete("memberUid", []string{userUID}) err := l.Modify(modReq) if err != nil && !ldap.IsErrorWithCode(err, ldap.LDAPResultNoSuchAttribute) { - return fmt.Errorf("Failed to remove user %s from group %s: %v", userUID, groupDN, err) + return fmt.Errorf("failed to remove user %s from group %s: %w", userUID, groupDN, err) } return nil @@ -191,7 +190,7 @@ func GetAllUsersLDAP(l *ldap.Conn) (*ldap.SearchResult, error) { // create the LDAP search request req := ldap.NewSearchRequest( - "dc=c3local", // search base + STD_DC, // search base ldap.ScopeWholeSubtree, // scope ldap.NeverDerefAliases, // aliases 0, 0, // size/time limit @@ -205,7 +204,7 @@ func GetAllUsersLDAP(l *ldap.Conn) (*ldap.SearchResult, error) { // perform the search sr, err := l.Search(req) if err != nil { - err = fmt.Errorf("Failed to fetch users from LDAP: %v", err) + err = fmt.Errorf("failed to fetch users from LDAP: %w", err) return sr, err } @@ -217,7 +216,7 @@ func GetAllGroupsLDAP(l *ldap.Conn) (map[string]string, error) { // build search req := ldap.NewSearchRequest( - "dc=c3local", + STD_DC, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, @@ -229,7 +228,7 @@ func GetAllGroupsLDAP(l *ldap.Conn) (map[string]string, error) { // search sr, err := l.Search(req) if err != nil { - return nil, fmt.Errorf("LDAP search failed: %w", err) + return nil, fmt.Errorf(SEARCH_FAIL, err) } // arrange result into a map string->string @@ -249,7 +248,7 @@ func GetAllUIDsLDAP(l *ldap.Conn) ([]int, error) { // build search req := ldap.NewSearchRequest( - "dc=c3local", + STD_DC, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, @@ -261,7 +260,7 @@ func GetAllUIDsLDAP(l *ldap.Conn) ([]int, error) { // search sr, err := l.Search(req) if err != nil { - return nil, fmt.Errorf("LDAP search failed: %w", err) + return nil, fmt.Errorf(SEARCH_FAIL, err) } // arrange result into a array of integers @@ -291,7 +290,7 @@ func GetNewUIDNumLDAP(l *ldap.Conn) (string, error) { res, err := utils.GetMEX(uids, MIN_UID, MAX_UID) if err != nil { - return "", fmt.Errorf("Error generating new uidNumber: %w", err) + return "", fmt.Errorf("error generating new uidNumber: %w", err) } return strconv.Itoa(res), nil @@ -314,7 +313,7 @@ func GetAllGIDsLDAP(l *ldap.Conn) ([]int, error) { // search sr, err := l.Search(req) if err != nil { - return nil, fmt.Errorf("LDAP search failed: %w", err) + return nil, fmt.Errorf(SEARCH_FAIL, err) } // arrange result into a array of integers @@ -344,7 +343,7 @@ func GetNewGIDNumLDAP(l *ldap.Conn) (string, error) { res, err := utils.GetMEX(gids, MIN_GID, MAX_GID) if err != nil { - return "", fmt.Errorf("Error generating new gidNumber: %w", err) + return "", fmt.Errorf("error generating new gidNumber: %w", err) } return strconv.Itoa(res), nil diff --git a/internal/entities/opts/opts.go b/internal/entities/opts/opts.go index 42777bf..277817e 100644 --- a/internal/entities/opts/opts.go +++ b/internal/entities/opts/opts.go @@ -57,112 +57,67 @@ func (o *Opts) ToString() string { } func (o *Opts) RetrieveOpts(cmd *cobra.Command) error { - var err error - - o.GRR, err = getFlagString(cmd, "grr") - if err != nil { - return err - } - - o.Resp, err = getFlagString(cmd, "resp") - if err != nil { - return err - } - - o.Name, err = getFlagString(cmd, "name") - if err != nil { - return err - } - - o.GID, err = getFlagString(cmd, "group") - if err != nil { - return err - } - - o.UID, err = getFlagString(cmd, "login") - if err != nil { - return err - } - - o.Ltype, err = getFlagString(cmd, "type") - if err != nil { - return err - } - - o.Shell, err = getFlagString(cmd, "shell") - if err != nil { - return err - } - - o.Webdir, err = getFlagBool(cmd, "web") - if err != nil { - return err - } - - o.Path, err = getFlagString(cmd, "path") - if err != nil { - return err - } - - o.Nobkp, err = getFlagString(cmd, "nobkp") - if err != nil { - return err - } - - o.Status, err = getFlagString(cmd, "status") - if err != nil { - return err - } - - o.Expiry, err = getFlagString(cmd, "expiry") - if err != nil { - return err - } - - o.Passwd, err = getFlagString(cmd, "passwd") - if err != nil { - return err - } - - o.Homedir, err = getFlagString(cmd, "homedir") - if err != nil { - return err - } - - o.Confirm, err = getFlagBool(cmd, "confirm") - if err != nil { - return err - } - - o.Unblock, err = getFlagBool(cmd, "unblock") - if err != nil { - return err - } - - o.Block, err = getFlagBool(cmd, "block") - if err != nil { - return err - } - - o.Ignore, err = getFlagBool(cmd, "ignore") - if err != nil { - return err - } - - o.Exact, err = getFlagBool(cmd, "exact") - if err != nil { - return err - } - - o.Number, err = getFlagInt(cmd, "number") - if err != nil { - return err + strFlags := map[string]*string{ + "grr": &o.GRR, + "path": &o.Path, + "resp": &o.Resp, + "name": &o.Name, + "type": &o.Ltype, + "group": &o.GID, + "login": &o.UID, + "shell": &o.Shell, + "nobkp": &o.Nobkp, + "status": &o.Status, + "expiry": &o.Expiry, + "passwd": &o.Passwd, + "homedir": &o.Homedir, + } + + boolFlags := map[string]*bool{ + "web": &o.Webdir, + "block": &o.Block, + "exact": &o.Exact, + "ignore": &o.Ignore, + "confirm": &o.Confirm, + "unblock": &o.Unblock, + } + + intFlags := map[string]*int{ + "number": &o.Number, + } + + for name, ptr := range strFlags { + val, err := getFlagString(cmd, name) + if err != nil { + return fmt.Errorf("%s: %w", name, err) + } + *ptr = val + } + + for name, ptr := range boolFlags { + val, err := getFlagBool(cmd, name) + if err != nil { + return fmt.Errorf("%s: %w", name, err) + } + *ptr = val + } + + for name, ptr := range intFlags { + val, err := getFlagInt(cmd, name) + if err != nil { + return fmt.Errorf("%s: %w", name, err) + } + *ptr = val } return nil } -// since this model is used in most subcommands, some +const ( + FLAG_RET_FAIL = "failed to get flag %q: %w" +) + +// since this entity is used in most subcommands, some // flags may exist in one command but not in another, // so if it doesn't exist just set as empty before // the error occurs, it won't be used anyway... :D @@ -174,7 +129,7 @@ func getFlagString(cmd *cobra.Command, flag string) (string, error) { flagValue, err := cmd.Flags().GetString(flag) if err != nil { - return "", fmt.Errorf("failed to get flag %q: %w", flag, err) + return "", fmt.Errorf(FLAG_RET_FAIL, flag, err) } return flagValue, nil } @@ -186,7 +141,7 @@ func getFlagInt(cmd *cobra.Command, flag string) (int, error) { flagValue, err := cmd.Flags().GetInt(flag) if err != nil { - return 0, fmt.Errorf("failed to get flag %q: %w", flag, err) + return 0, fmt.Errorf(FLAG_RET_FAIL, flag, err) } return flagValue, nil } @@ -198,7 +153,7 @@ func getFlagBool(cmd *cobra.Command, flag string) (bool, error) { flagValue, err := cmd.Flags().GetBool(flag) if err != nil { - return false, fmt.Errorf("failed to get flag %q: %w", flag, err) + return false, fmt.Errorf(FLAG_RET_FAIL, flag, err) } return flagValue, nil } diff --git a/internal/manage/manage.go b/internal/manage/manage.go index f1c9d85..5356ead 100644 --- a/internal/manage/manage.go +++ b/internal/manage/manage.go @@ -18,7 +18,7 @@ import ( const ( STD_PERM = "0700" // default user dir permission BLK_PERM = "0000" // user dir permission if blocked - MAX_VARIANCE = 60 // stops iteration at some point + MAX_VARIANCE = 60 // attempts at login generation HTML_BASE_PERM = "0755" // set public_html to this on creation NUM_GECOS_FIELDS = 8 ) @@ -117,20 +117,22 @@ search made: "useradm user show -l %v -e"`, login) return &u, nil } +const FAILED_PERMISSION = "failed to set permissions: %w" + func Block(u *user.User) error { err := auth.KRBRandKey(u.UID) // new password, no one will know if err != nil { return err } - cmd := exec.Command("chmod", "0000", u.Homedir) + cmd := exec.Command("chmod", BLK_PERM, u.Homedir) if err := cmd.Run(); err != nil { - return fmt.Errorf("Failed to set permissions: %w", err) + return fmt.Errorf(FAILED_PERMISSION, err) } - cmd = exec.Command("chmod", "0000", u.Nobackup) + cmd = exec.Command("chmod", BLK_PERM, u.Nobackup) if err := cmd.Run(); err != nil { - return fmt.Errorf("Failed to set permissions: %w", err) + return fmt.Errorf(FAILED_PERMISSION, err) } l, err := auth.ConnLDAP() @@ -151,14 +153,14 @@ func Block(u *user.User) error { } func Unblock(u *user.User, pass string) error { - cmd := exec.Command("chmod", "0700", u.Homedir) + cmd := exec.Command("chmod", STD_PERM, u.Homedir) if err := cmd.Run(); err != nil { - return fmt.Errorf("Failed to set permissions: %w", err) + return fmt.Errorf(FAILED_PERMISSION, err) } - cmd = exec.Command("chmod", "0700", u.Nobackup) + cmd = exec.Command("chmod", STD_PERM, u.Nobackup) if err := cmd.Run(); err != nil { - return fmt.Errorf("Failed to set permissions: %w", err) + return fmt.Errorf(FAILED_PERMISSION, err) } l, err := auth.ConnLDAP() @@ -171,7 +173,7 @@ func Unblock(u *user.User, pass string) error { mod.Replace("loginShell", []string{"/bin/bash"}) if err := l.Modify(mod); err != nil { - return fmt.Errorf("Failed to update loginShell for %s: %w", u.UID, err) + return fmt.Errorf("failed to update loginShell for %s: %w", u.UID, err) } if pass == "_" { @@ -310,25 +312,25 @@ func CreateHome(u *user.User, homeDir string) error { cmd.Stdout = nil cmd.Stderr = nil if err := cmd.Run(); err != nil { - return fmt.Errorf("Failed to create home directory: %w", err) + return fmt.Errorf("failed to create home directory: %w", err) } // copy /etc/skel cmd = exec.Command("cp", "-r", "/etc/skel/.", homeDir) if err := cmd.Run(); err != nil { - return fmt.Errorf("Failed to copy /etc/skel contents: %w", err) + return fmt.Errorf("failed to copy /etc/skel contents: %w", err) } // change permissions cmd = exec.Command("chmod", STD_PERM, homeDir) if err := cmd.Run(); err != nil { - return fmt.Errorf("Failed to set permissions: %w", err) + return fmt.Errorf("failed to set permissions: %w", err) } // change ownership cmd = exec.Command("chown", "-R", fmt.Sprintf("%s:%s", u.UID, u.GID), homeDir) if err := cmd.Run(); err != nil { - return fmt.Errorf("Failed to change ownership: %w", err) + return fmt.Errorf("failed to change ownership: %w", err) } return nil @@ -353,37 +355,37 @@ func CreateWeb(u *user.User) error { cmd.Stdout = nil cmd.Stderr = nil if err := cmd.Run(); err != nil { - return fmt.Errorf("Failed to create web directory: %w", err) + return fmt.Errorf("failed to create web directory: %w", err) } // create index cmd = exec.Command("touch", filepath.Join(u.Webdir, "index.html")) if err := cmd.Run(); err != nil { - return fmt.Errorf("Failed to create index.html: %w", err) + return fmt.Errorf("failed to create index.html: %w", err) } // create link in users home cmd = exec.Command("ln", "-s", u.Webdir, filepath.Join(u.Homedir, "public_html")) if err := cmd.Run(); err != nil { - return fmt.Errorf("Failed to create link public_html: %w", err) + return fmt.Errorf("failed to create link public_html: %w", err) } // change permissions cmd = exec.Command("chmod", STD_PERM, u.Webdir) if err := cmd.Run(); err != nil { - return fmt.Errorf("Failed to set permissions: %w", err) + return fmt.Errorf("failed to set permissions: %w", err) } // change ownership cmd = exec.Command("chown", "-R", fmt.Sprintf("%s:%s", u.UID, u.GID), u.Webdir) if err := cmd.Run(); err != nil { - return fmt.Errorf("Failed to change ownership: %w", err) + return fmt.Errorf("failed to change ownership: %w", err) } // set permission for public_html cmd = exec.Command("chmod", HTML_BASE_PERM, filepath.Join(u.Homedir, "public_html")) if err := cmd.Run(); err != nil { - return fmt.Errorf("Failed to create set permissions for public_html: %w", err) + return fmt.Errorf("failed to create set permissions for public_html: %w", err) } success = true @@ -460,7 +462,7 @@ func GenMissingFields(u *user.User) error { if u.UIDNumber == "" { uidNum, err := auth.GetNewUIDNumLDAP(l) if err != nil { - return fmt.Errorf("Failed to generate new UIDNumber for user: %v", err) + return fmt.Errorf("failed to generate new UIDNumber for user: %v", err) } u.UIDNumber = uidNum } diff --git a/internal/validate/validate.go b/internal/validate/validate.go index 9ed81a5..c05ea9d 100644 --- a/internal/validate/validate.go +++ b/internal/validate/validate.go @@ -2,6 +2,7 @@ package validate import ( "fmt" + "maps" "regexp" "strings" @@ -10,65 +11,247 @@ import ( "gitlab.c3sl.ufpr.br/tss24/useradm/pkg/utils" ) +const ( + AlreadyExistsLDAP = "already exists in LDAP" + CantBeEmpty = "cannot be empty" +) + +type ValidationError struct { + Field string + Value interface{} + Message string +} + +func (e ValidationError) Error() string { + return fmt.Sprintf("%s: %v - %s", e.Field, e.Value, e.Message) +} + +type MultiValidationError []ValidationError + +func (e MultiValidationError) Error() string { + var sb strings.Builder + sb.WriteString("Validation failed:\n") + for _, err := range e { + sb.WriteString(fmt.Sprintf("- %s\n", err.Error())) + } + return sb.String() +} + func Expiry(expiry string) error { + if expiry == "" { + return ValidationError{ + Field: "expiry", + Value: expiry, + Message: CantBeEmpty, + } + } + if expiry == "_" { return nil } - parts := strings.Split(expiry, ".") - if !utils.IsValidDate(parts) { - err := fmt.Errorf("Malformed expiry date string, use \"dd.mm.yy\"") - return err + if !utils.IsValidDate(strings.Split(expiry, ".")) { + return ValidationError{ + Field: "expiry", + Value: expiry, + Message: "must be in dd.mm.yy format", + } } + return nil } -// TODO: change this check func Status(status string) error { - if status != "Blocked" && status != "Active" { - return fmt.Errorf("User status can only be \"Active\" or \"Blocked\"") + ok := map[string]bool{"Active": true, "Blocked": true} + if status == "" { + return ValidationError{ + Field: "status", + Value: status, + Message: CantBeEmpty, + } + } + + if !ok[status] { + return ValidationError{ + Field: "status", + Value: status, + Message: fmt.Sprintf("must be one of: %v", maps.Keys(ok)), + } } return nil } func GRR(users []user.User, grr string) error { - // OK if empty, only "ini" login type requires it and we check :) + if grr == "" { + return ValidationError{ + Field: "grr", + Value: grr, + Message: CantBeEmpty, + } + } + if grr == "_" { return nil } - isValid, _ := regexp.MatchString(`^\d{8}$`, grr) // is 8 digit number - if !isValid { - err := fmt.Errorf("Malformed GRR string, must be 8 digit number") - return err + if match, _ := regexp.MatchString(`^\d{8}$`, grr); !match { + return ValidationError{ + Field: "grr", + Value: grr, + Message: "must be an 8-digit number", + } } if manage.GRRExists(users, grr) { - err := fmt.Errorf(`The informed GRR already exists in LDAP database -Note: To search for the account use "useradm user show -r %s"`, grr) + return ValidationError{ + Field: "grr", + Value: grr, + Message: AlreadyExistsLDAP, + } + } + + return nil +} + +func GroupUnique(groups map[string]string, group string) error { + if group == "" { + return ValidationError{ + Field: "group", + Value: group, + Message: CantBeEmpty, + } + } + + for _, v := range groups { + if v == group { + return ValidationError{ + Field: "group", + Value: group, + Message: AlreadyExistsLDAP, + } + } + } + + return nil +} + +func Login(login string) error { + if login == "" { + return ValidationError{ + Field: "login", + Value: login, + Message: CantBeEmpty, + } + } + + return nil +} + +func LoginUnique(users []user.User, login string) error { + if err := Login(login); err != nil { return err } + if res := manage.Search(users, false, true, login, "", "", "", "", ""); len(res) > 0 { + return ValidationError{ + Field: "login", + Value: login, + Message: AlreadyExistsLDAP, + } + } + return nil } -func GID(groups map[string]string, group string) error { - var err error +func UserName(name string) error { + if name == "" { + return ValidationError{ + Field: "name", + Value: name, + Message: CantBeEmpty, + } + } - for _, value := range groups { - if value == group { - return nil + if name == "_" { + return ValidationError{ + Field: "name", + Value: name, + Message: "reserved placeholder value", } } - err = fmt.Errorf("Could't find group \"%v\" in LDAP database", group) - return err + return nil } -func UID(users []user.User, login string) error { - res := manage.Search(users, false, true, login, "", "", "", "", "") - if len(res) != 0 { - return fmt.Errorf(`The informed Login already exists in LDAP database -Note: To search for the account use "useradm user show -l %s"`, login) +func PathExists(path, field string) error { + if path == "" || path == "_" { + return ValidationError{ + Field: field, + Value: path, + Message: CantBeEmpty, + } + } + + if utils.PathExists(path) { + return ValidationError{ + Field: field, + Value: path, + Message: fmt.Sprintf("path %v already exists", path), + } + } + return nil +} + +func PathDoesntExist(path, field string) error { + if path == "" || path == "_" { + return ValidationError{ + Field: field, + Value: path, + Message: CantBeEmpty, + } } + + if !utils.PathExists(path) { + return ValidationError{ + Field: field, + Value: path, + Message: fmt.Sprintf("path %v doesn't exist", path), + } + } + + return nil +} + +func UserNameUnique(users []user.User, name string) error { + if err := UserName(name); err != nil { + return err + } + + if res := manage.Search(users, true, true, "", "", name, "", "", ""); len(res) > 0 { + return ValidationError{ + Field: "name", + Value: name, + Message: AlreadyExistsLDAP, + } + } + return nil +} + +func All(validators ...func() error) error { + var errs MultiValidationError + + for _, validator := range validators { + if err := validator(); err != nil { + if verr, ok := err.(ValidationError); ok { + errs = append(errs, verr) + } else { + return fmt.Errorf("unexpected error type: %w", err) + } + } + } + + if len(errs) > 0 { + return errs + } + return nil } -- GitLab