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