From 1a4f8870723b30709f413e157877f8a2fe9c117a Mon Sep 17 00:00:00 2001
From: Theo S Schult <theo.sschult@gmail.com>
Date: Wed, 19 Feb 2025 09:11:26 -0300
Subject: [PATCH] Finish user create prototype

---
 .gitignore         |   1 -
 cmd/root.go        |   3 +-
 cmd/user.go        |   2 +-
 cmd/user/create.go | 686 ++++++++++++++++++++++++++++++++++++++++-----
 cmd/user/delete.go |  62 +++-
 cmd/user/mod.go    |  30 +-
 cmd/user/show.go   | 239 +++++++++++-----
 model/user.go      |  52 +++-
 8 files changed, 904 insertions(+), 171 deletions(-)

diff --git a/.gitignore b/.gitignore
index 142bcc0..590d1aa 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,3 @@
 useradm
-build.sh
 useradm.py
 full_commands.py
diff --git a/cmd/root.go b/cmd/root.go
index ac746c1..4e7fad1 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -14,12 +14,13 @@ var rootCmd = &cobra.Command{
 with the cobra library that took inspiration from the old 
 useradm.py. It takes care of LDAP configuration and was 
 made as an update for the previous version.`,
+    SilenceErrors: true,
 }
 
 func Execute() {
 	err := rootCmd.Execute()
 	if err != nil {
-		fmt.Fprintf(os.Stderr, "cmd: %v", err)
+		fmt.Fprintf(os.Stderr, "ERROR: %v\n", err)
 		os.Exit(1)
 	}
 }
diff --git a/cmd/user.go b/cmd/user.go
index 3208b8d..9ca388f 100644
--- a/cmd/user.go
+++ b/cmd/user.go
@@ -11,7 +11,7 @@ var userCmd = &cobra.Command{
 }
 
 func init() {
-	userCmd.AddCommand(user.CreateCmd)
+	userCmd.AddCommand(user.CreateUserCmd)
 	userCmd.AddCommand(user.DeleteCmd)
 	userCmd.AddCommand(user.ModCmd)
 	userCmd.AddCommand(user.ShowCmd)
diff --git a/cmd/user/create.go b/cmd/user/create.go
index 57a5e16..56f936c 100644
--- a/cmd/user/create.go
+++ b/cmd/user/create.go
@@ -1,16 +1,32 @@
 package user
 
 import (
-	"os"
 	"fmt"
-	"bufio"
+	"log"
+	"os"
+	"os/exec"
+	"path/filepath"
+	"regexp"
+	"sort"
+	"strconv"
 	"strings"
+	"time"
 
+	"github.com/go-ldap/ldap/v3"
 	"github.com/spf13/cobra"
 	"gitlab.c3sl.ufpr.br/tss24/useradm/model"
 )
 
-var CreateCmd = &cobra.Command{
+const (
+    MAIL_ALIAS_FILE = "/etc/aliases"
+    DEF_PERMISSION  = 0700
+    BLK_PERMISSION  = 0000
+    MAX_VARIANCE    = 50
+    MIN_UID         = 1000
+    MAX_UID         = 6000
+)
+
+var CreateUserCmd = &cobra.Command{
 	Use:   "create",
 	Short: "Create new user",
 	RunE:  createUserFunc,
@@ -18,103 +34,629 @@ var CreateCmd = &cobra.Command{
 
 func init() {
 	// Possible Flags
-	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("path", "w", "", "Path to webdir      (e.g. path/login)")
-	CreateCmd.Flags().StringP("name", "n", "", "User full name      (use quotes for spaces)")
-	CreateCmd.Flags().StringP("login", "l", "", "User login name     (auto-generated if empty)")
-	CreateCmd.Flags().StringP("group", "g", "", "User initial group")
-	CreateCmd.Flags().StringP("shell", "s", "/bin/bash", "Full path to shell ")
-	CreateCmd.Flags().StringP("homedir", "d", "", "Home directory path (/home/group/login if empty)")
-	CreateCmd.Flags().StringP("password", "p", "", "User password       (auto-generated if empty)")
-	CreateCmd.Flags().StringP("status", "a", "Free", "User status      (Blocked/Free)")
+	CreateUserCmd.Flags().StringP("grr", "r", "_", "User GRR, required for ini type")
+	CreateUserCmd.Flags().StringP("type", "t", "ini", "Type of auto-generated login: ini, first or last")
+	CreateUserCmd.Flags().StringP("path", "w", "", "Full path to webdir, if not set don't create")
+	CreateUserCmd.Flags().StringP("name", "n", "_", "User full name, required, use quotes for spaces")
+	CreateUserCmd.Flags().StringP("login", "l", "", "User login name, auto-generated if empty")
+	CreateUserCmd.Flags().StringP("group", "g", "", "User base group, required")
+	CreateUserCmd.Flags().StringP("shell", "s", "/bin/bash", "Full path to shell")
+	CreateUserCmd.Flags().StringP("homedir", "d", "", "Home directory path, /home/group/login if empty")
+	CreateUserCmd.Flags().StringP("passwd", "p", "", "User password, auto-generated if empty")
+	CreateUserCmd.Flags().StringP("status", "a", "Active", "User status, Active or Blocked")
+	CreateUserCmd.Flags().StringP("resp", "i", "_", "Person responsible for the account")
+	CreateUserCmd.Flags().StringP("expiry", "e", "_", "Expiry date in format dd.mm.yy")
+	CreateUserCmd.Flags().StringP("course", "c", "_", "User course/minicourse, even temp ones")
+	CreateUserCmd.Flags().StringP("nobkp", "b", "", "User nobackup directory path")
 
 	// Required Flags
-	CreateCmd.MarkFlagRequired("name")
-	CreateCmd.MarkFlagRequired("type")
-	CreateCmd.MarkFlagRequired("group")
+	CreateUserCmd.MarkFlagRequired("name")
+	CreateUserCmd.MarkFlagRequired("group")
 
-	CreateCmd.Flags().BoolP("confirm", "y", false, "Skip confirmation prompt")
+	CreateUserCmd.Flags().BoolP("confirm", "y", false, "Skip confirmation prompt")
 }
 
 func createUserFunc(cmd *cobra.Command, args []string) error {
-	confirm, err := cmd.Flags().GetBool("confirm")
-	if err != nil {
-		return err
-	}
+    success := false
+    // Creates model from users input
+    usr, confirm, err := createNewUserModel(cmd)
+    if err != nil { return err }
+
+    defer func() {
+        if !success {
+            _ = delKerberosPrincipal(usr.UID)
+            _ = delUserLDAP(usr)
+        }
+    }()
+
+    // Prints info for confirmation
+	fmt.Printf("%v\n", usr.FullToString())
+	//fmt.Printf("%v\n	Passwd:  %v\n\n", usr.ToString(), usr.Password)
+    confirmationPrompt(confirm, "creation")
+
+    // Generate the new password
+    if usr.Password == "[auto-generate]" {
+        usr.Password = genPassword()
+    }
+
+    err = addUserLDAP(usr)
+    if err != nil { return err }
+
+    err = addKerberosPrincipal(usr.UID)
+    if err != nil { return err }
+
+    err = modKerberosPassword(usr.UID, usr.Password)
+    if err != nil { return err }
+
+    err = createUserDirs(usr)
+    if err != nil { return err }
+
+	fmt.Println("Done!")
+    fmt.Printf("\nUser Login:     %v", usr.UID)
+    fmt.Printf("\nUser Password:  %v\n\n", usr.Password)
+    fmt.Printf("%v:%v:%v:%v:%v:%v:%v:%v:%v:\n", usr.UID, usr.Password, usr.GID,
+        usr.Name, usr.Gecos, usr.GRR, usr.Shell, usr.Homedir, usr.Webdir)
+
+    success = true
+	return nil
+}
+
+func createNewUserModel(cmd *cobra.Command) (model.User, bool, error) {
+    var u model.User
+    users, err := getUsers()
+	if err != nil { return u, false, err }
+	userGRR, err := cmd.Flags().GetString("grr")
+	if err != nil { return u, false, err }
+	userResp, err := cmd.Flags().GetString("resp")
+	if err != nil { return u, false, err }
 	userName, err := cmd.Flags().GetString("name")
-	if err != nil {
-		return err
-	}
+	if err != nil { return u, false, err }
 	userType, err := cmd.Flags().GetString("type")
-	if err != nil {
-		return err
-	}
+	if err != nil { return u, false, err }
 	userGID, err := cmd.Flags().GetString("group")
-	if err != nil {
-		return err
-	}
-	userGRR, err := cmd.Flags().GetString("grr")
-	if err != nil {
-		return err
-	}
-	userWebdir, err := cmd.Flags().GetString("path")
-	if err != nil {
-		return err
-	}
-	userLogin, err := cmd.Flags().GetString("login")
-	if err != nil {
-		return err
-	}
+	if err != nil { return u, false, err }
+	userUID, err := cmd.Flags().GetString("login")
+	if err != nil { return u, false, err }
+	confirm, err := cmd.Flags().GetBool("confirm")
+	if err != nil { return u, false, err }
 	userShell, err := cmd.Flags().GetString("shell")
-	if err != nil {
-		return err
-	}
-	userHomedir, err := cmd.Flags().GetString("homedir")
-	if err != nil {
-		return err
-	}
-	userPassword, err := cmd.Flags().GetString("password")
-	if err != nil {
-		return err
-	}
+	if err != nil { return u, false, err }
+	userWebdir, err := cmd.Flags().GetString("path")
+	if err != nil { return u, false, err }
 	userStatus, err := cmd.Flags().GetString("status")
-	if err != nil {
-		return err
-	}
+	if err != nil { return u, false, err }
+	userCourse, err := cmd.Flags().GetString("course")
+	if err != nil { return u, false, err }
+	userExpiry, err := cmd.Flags().GetString("expiry")
+	if err != nil { return u, false, err }
+	userHomedir, err := cmd.Flags().GetString("homedir")
+	if err != nil { return u, false, err }
+	userPassword, err := cmd.Flags().GetString("passwd")
+	if err != nil { return u, false, err }
+	userNobackup, err := cmd.Flags().GetString("nobkp")
+	if err != nil { return u, false, err }
+
+    groups, err := getGroups()
+    if err != nil { return u, false, err }
+
+    err = validateExpiry(userExpiry)
+	if err != nil { return u, false, err }
+
+    err = validateStatus(userStatus)
+	if err != nil { return u, false, err }
+
+    err = validateGID(groups, userGID)
+	if err != nil { return u, false, err }
+
+    err = validateUID(users, userUID)
+	if err != nil { return u, false, err }
+
+    err = validateGRR(users, userGRR)
+	if err != nil { return u, false, err }
 
-	user := model.User{
-		UID:      ifThenElse(userLogin != "", userLogin, "[auto-generate]"),
+    if userStatus == "Blocked" { userShell = "/bin/false" }
+
+    if userType == "ini" && userGRR == "_" {
+        err := fmt.Errorf("GRR is required for \"ini\" login type")
+		return u, false, err
+    }
+
+    if userUID == "" { 
+        used, variance := true, 0
+        for used {
+            userUID = genLogin(userName, userGRR, userType, variance)
+            used = userUID == "" || userUID == "_" ||
+                   loginExists(users, userUID) ||
+                   mailAliasExists(userUID) 
+            variance++
+            if variance > MAX_VARIANCE {
+                log.Fatalf("Could't generate login automatically, please inform the desired login\n")
+            }
+        }
+    }
+
+    userHomedir = genHomedirPath(userUID, userHomedir, userGID)
+    err = validatePath(userHomedir)
+    if err != nil { return u, false, err }
+
+    userNobackup = genNobackupPath(userUID, userGID, userNobackup)
+    err = validatePath(userNobackup)
+    if err != nil { return u, false, err }
+
+    userWebdir = genWebdirPath(userUID, userWebdir)
+    err = validatePath(userWebdir)
+    if err != nil { return u, false, err }
+
+	u = model.User{
+		UID:      userUID,
 		GID:      userGID,
 		GRR:      userGRR,
+        Resp:     userResp,
 		Name:     userName,
 		Ltype:    userType,
 		Shell:    userShell,
-		Webdir:   userWebdir,
         Status:   userStatus,
+        Expiry:   userExpiry,
+        Course:   userCourse,
+		Webdir:   userWebdir,
 		Homedir:  userHomedir,
+		Nobackup: userNobackup,
 		Password: ifThenElse(userPassword != "", "[set]", "[auto-generate]"),
 	}
 
-	fmt.Println(user.ToString())
-	fmt.Printf ("	Passwd:  %v", user.Password)
+    u = genGecos(u)
+    
+    newUIDNumber, err := getNewUIDNumber()
+    if err != nil {
+        err = fmt.Errorf("Failed to generate new UIDNumber for user: %v", err)
+        return u, false, err
+    }
+    u.UIDNumber = newUIDNumber
 
-	if !confirm {
-		fmt.Print("Proceed with user creation? [y/N] ")
-		reader := bufio.NewReader(os.Stdin)
-		response, _ := reader.ReadString('\n')
-		if strings.TrimSpace(strings.ToLower(response)) != "y" {
-			fmt.Fprintln(os.Stderr, "Aborted.")
-			os.Exit(1)
-		}
-	}
+    for key, value := range groups {
+        if value == u.GID {
+            u.GIDNumber = key
+        }
+    } 
 
-	fmt.Println("Done!")
+    u.DN = "uid=" + u.UID + ",ou=usuarios,dc=c3local"
 
-	return nil
+    return u, confirm, nil
 }
 
 func ifThenElse(condition bool, a string, b string) string {
 	if condition { return a }
 	return b
 }
+
+func genHomedirPath(login string, homedir string, group string) string {
+    if homedir != "" { return homedir }
+    return filepath.Join("/home", group, login)
+}
+
+func genWebdirPath(login string, webdir string) string {
+    if webdir != "" { return webdir }
+    return filepath.Join("/home/html/inf", login)
+}
+
+func genNobackupPath(login string, group string, nobkp string) string {
+    if nobkp != "" { return nobkp }
+    return filepath.Join("/nobackup", group, login)
+}
+
+func genGecos(user model.User) model.User {
+    gecos := user.UID + ","
+    gecos += user.GRR + ","
+    gecos += user.Resp + ","
+    gecos += user.Course + ","
+    gecos += user.Status + ","
+    gecos += user.Expiry + ","
+    gecos += user.Ltype + ","
+    gecos += user.Webdir + ","
+    gecos += user.Nobackup
+    user.Gecos = gecos
+    return user
+}
+
+func validatePath(path string) error {
+    _, err := os.Stat(path)
+    if os.IsNotExist(err) { return nil }
+    return fmt.Errorf("Path \"%v\" already exists, please provide a new path", path) 
+}
+
+func validateExpiry(expiry string) error {
+    if expiry == "_" { return nil }
+
+    parts := strings.Split(expiry, ".")
+    if !isValidDate(parts) {
+        err := fmt.Errorf("Malformed expiry date string, use \"dd.mm.yy\"")
+        return err
+    }
+    return nil
+}
+
+func isValidDate(arr []string) bool {
+	if len(arr) != 3 {
+		return false
+	}
+
+	// Convert to int
+	day, err1 := strconv.Atoi(arr[0])
+	month, err2 := strconv.Atoi(arr[1])
+	year, err3 := strconv.Atoi(arr[2])
+
+	if err1 != nil || err2 != nil || err3 != nil {
+		return false
+	}
+
+	// Ensure year is two digits 
+	if year < 0 || year > 99 {
+		return false
+	}
+
+	// Validate the date
+	fullYear := 2000 + year 
+	t := time.Date(fullYear, time.Month(month), day, 0, 0, 0, 0, time.UTC)
+	return t.Day() == day && t.Month() == time.Month(month)
+}
+
+func genLoginIni(name string, grr string, variance int) string {
+    login := ""
+    parts := formatName(name)
+
+    if len(parts) == 0 {
+        return "_"
+    }
+    x := variance / len(parts)
+    y := variance % len(parts)
+
+    for i := range parts {
+        if y != 0 {
+            login += parts[i][:x+2]  
+            y--
+        } else {
+            login += parts[i][:x+1]
+        }
+    }
+
+	login += grr[2:4]
+
+	return login
+}
+
+func genLoginFirst(name string, variance int) string {
+    parts := formatName(name)
+
+    if len(parts) == 0 {
+        return "_"
+    }
+
+    // Separate first name
+    first, parts := parts[0], parts[1:]
+    login := first
+
+    variance++
+    x := variance / len(parts)
+    y := variance % len(parts)
+
+    for i := range parts {
+        if y != 0 {
+            login += parts[i][:x+1]
+            y--
+        } else {
+            login += parts[i][:x]
+        }
+    }
+
+    return login
+}
+
+func genLoginLast(name string, variance int) string {
+    login := ""
+    parts := formatName(name)
+
+    if len(parts) <= 1 {
+        return "_"
+    }
+
+    // Separate last name
+    last, parts := parts[len(parts)-1], parts[:len(parts)-1]
+
+    x := variance / len(parts)
+    y := variance % len(parts)
+
+    for i := range parts {
+        if y != 0 {
+            login += parts[i][:x+2]
+            y--
+        } else {
+            login += parts[i][:x+1]
+        }
+    }
+
+    login += last
+
+    return login
+}
+
+func genLogin(name string, grr string, ltype string, variance int) string {
+    var login string
+    switch ltype {
+    case "", "ini":
+        login = genLoginIni(name, grr, variance)
+    case "first":
+        login = genLoginFirst(name, variance)
+    case "last":
+        login = genLoginLast(name, variance)
+    }
+    return login
+}
+
+func validateStatus(status string) error {
+    if status != "Blocked" && status != "Active" {
+        err := fmt.Errorf("User status can only be \"Active\" or \"Blocked\"")
+        return err
+    }
+    return nil
+}
+
+func validateGRR(users []model.User, grr string) error {
+    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 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 err
+    }
+
+    return nil
+}
+
+func validateGID(groups map[string]string, group string) error {
+    for _, value := range groups {
+        if value == group {
+            return nil
+        }
+    }
+    err := fmt.Errorf("Could't find group \"%v\" in LDAP database", group)
+    return err
+}
+
+func validateUID(users []model.User, login string) error {
+    for i := range users {
+        if users[i].UID == login {
+            err := fmt.Errorf(`The informed Login already exists in LDAP database
+Note: To search for the account use "useradm user show -l %s"`, login)
+            return err
+        }
+    }
+    return nil
+}
+
+func formatName(name string) []string {
+    connectives := map[string]bool{"da": true, "de": true, "di": true, 
+		            "do": true, "das": true, "dos": true, "von": true}
+	splitName := strings.Fields(name)
+	var parts []string
+
+	for _, part := range splitName {
+		lowerPart := strings.ToLower(part)
+		if !connectives[lowerPart] {
+			parts = append(parts, string(lowerPart))
+		}
+	}
+    return parts
+}
+
+// Queries to check if the alias exists
+func mailAliasExists(alias string) bool {
+    cmd := exec.Command("/usr/sbin/postalias", "-q", alias, MAIL_ALIAS_FILE)
+    cmd.Stdout = nil
+    cmd.Stderr = nil
+    return cmd.Run() == nil
+} 
+
+func getNewUIDNumber() (string, error) {
+    var avail int
+
+    uids, err := getUIDs()
+
+    if err != nil { return "", err }
+
+    for i := MIN_UID; i <= MAX_UID; i++ {
+        avail = sort.Search(len(uids), func(j int) bool {
+    	    return uids[j] >= i
+        })
+    }
+
+    if avail > MAX_UID { 
+        err = fmt.Errorf("No more available UIDNumbers")
+        return "", err 
+    }
+
+    uid := strconv.Itoa(avail)
+    return uid, nil
+}
+
+func addUserLDAP(u model.User) error {
+    l, err := connLDAP()
+    if err != nil {
+        return err
+    }
+    defer l.Close()
+
+	req := ldap.NewAddRequest(u.DN, nil)
+	req.Attribute("uid", []string{u.UID})
+	req.Attribute("cn", []string{u.Name})
+	req.Attribute("objectClass", []string{"account", "posixAccount"})
+	req.Attribute("loginShell",  []string{u.Shell})
+	req.Attribute("uidNumber",   []string{u.UIDNumber})
+	req.Attribute("gidNumber",   []string{u.GIDNumber})
+	req.Attribute("homeDirectory", []string{u.Homedir})
+	req.Attribute("gecos", []string{u.Gecos})
+
+    err = l.Add(req)
+    if err != nil {
+        return fmt.Errorf("Failed to add user %s to LDAP: %v", u.UID, err)
+    }
+
+    return nil
+}
+
+func addKerberosPrincipal(login string) error {
+	cmd := exec.Command("kadmin.local", "-q",
+		fmt.Sprintf("addprinc -policy padrao -randkey -x dn=uid=%s,ou=usuarios,dc=c3local %s", login, login))
+
+	output, err := cmd.CombinedOutput()
+	if err != nil {
+        return fmt.Errorf("Failed to add Kerberos principal: %v\nOutput: %s", err, output)
+	}
+
+	return nil
+}
+
+func modKerberosPassword(login, password string) error {
+	cmd := exec.Command("kadmin.local", "-q",
+		fmt.Sprintf("cpw -pw %s %s", password, login))
+
+    cmd.Stdout = nil
+    cmd.Stderr = nil
+	output, err := cmd.CombinedOutput()
+	if err != nil {
+		return fmt.Errorf("Failed to change password for %s: %v\nOutput: %s", login, err, output)
+	}
+
+	return nil
+}
+
+func createUserDirs(u model.User) error {
+    success := false
+    defer func() {
+        if !success {
+            fmt.Println("Error found! Cleaning up...")
+            _ = DeleteHome(u.Nobackup)
+            _ = DeleteHome(u.Homedir)
+        }
+    }()
+
+    if err := createHome(u, u.Homedir)
+    err != nil { return err }
+
+    if err := createHome(u, u.Nobackup) 
+    err != nil { return err }
+
+    if err := createWeb(u)
+    err != nil { return err }
+
+    success = true
+    return nil
+}
+
+func DeleteHome(homeDir string) error {
+	return os.RemoveAll(homeDir)
+}
+
+func createHome(u model.User, homeDir string) error {
+    var perm os.FileMode
+
+    perm = DEF_PERMISSION
+    if u.Status == "Blocked" {
+        perm = BLK_PERMISSION
+    }
+
+	// Create directory
+	if err := os.MkdirAll(homeDir, perm); err != nil {
+		return fmt.Errorf("Failed to create home directory: %w", err)
+	}
+
+
+	// Copy /etc/skel
+	cmd := exec.Command("cp", "-r", "/etc/skel/.", homeDir)
+    cmd.Stdout = nil
+    cmd.Stderr = nil
+	if err := cmd.Run(); err != nil {
+		return fmt.Errorf("Failed to copy /etc/skel contents: %w", err)
+	}
+
+    uid, err := strconv.Atoi(u.UIDNumber)
+    if err != nil {
+        return fmt.Errorf("Failed to convert uid to int: %w", err)
+    }
+    gid, err := strconv.Atoi(u.GIDNumber)
+    if err != nil {
+        return fmt.Errorf("Failed to convert gid to int: %w", err)
+    }
+	// Change ownership
+	if err := os.Chown(homeDir, uid, gid); err != nil {
+		return fmt.Errorf("Failed to change ownership: %w", err)
+	}
+
+	// Change permissions
+	if err := os.Chmod(homeDir, perm); err != nil {
+		return fmt.Errorf("Failed to set permissions: %w", err)
+	}
+
+    return nil
+}
+
+func createWeb(u model.User) error {
+    var perm os.FileMode
+    success := false
+
+    perm = DEF_PERMISSION
+    if u.Status == "Blocked" {
+        perm = BLK_PERMISSION
+    }
+
+    defer func() {
+        if !success {
+            _ = DeleteHome(u.Webdir)
+        }
+    }()
+
+	// Create directory
+	if err := os.MkdirAll(u.Webdir, perm); err != nil {
+		return fmt.Errorf("Failed to create web directory: %w", err)
+	}
+
+	// Create index
+	cmd := exec.Command("touch", filepath.Join(u.Webdir, "index.html"))
+    cmd.Stdout = nil
+    cmd.Stderr = nil
+	if err := cmd.Run(); err != nil {
+		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)
+	}
+
+    uid, err := strconv.Atoi(u.UIDNumber)
+    if err != nil {
+        return fmt.Errorf("Failed to convert uid to int: %w", err)
+    }
+    gid, err := strconv.Atoi(u.GIDNumber)
+    if err != nil {
+        return fmt.Errorf("Failed to convert gid to int: %w", err)
+    }
+	// Change ownership
+	if err := os.Chown(u.Webdir, uid, gid); err != nil {
+		return fmt.Errorf("Failed to change ownership: %w", err)
+	}
+
+	// Change permissions
+	if err := os.Chmod(u.Webdir, perm); err != nil {
+		return fmt.Errorf("Failed to set permissions: %w", err)
+	}
+
+    success = true
+    return nil
+}
diff --git a/cmd/user/delete.go b/cmd/user/delete.go
index 2777308..b9e4bfa 100644
--- a/cmd/user/delete.go
+++ b/cmd/user/delete.go
@@ -1,8 +1,68 @@
 package user
 
-import "github.com/spf13/cobra"
+import (
+	"fmt"
+	"log"
+	"os/exec"
+
+	"github.com/go-ldap/ldap/v3"
+	"github.com/spf13/cobra"
+	"gitlab.c3sl.ufpr.br/tss24/useradm/model"
+)
 
 var DeleteCmd = &cobra.Command{
 	Use:   "delete",
 	Short: "Delete a user",
 }
+
+func delUserLDAP(u model.User) error {
+    l, err := connLDAP()
+    if err != nil {
+        return err
+    }
+    defer l.Close()
+
+    // Remove user from all groups 
+    searchReq := ldap.NewSearchRequest(
+        "ou=grupos,dc=c3local,dc=com", 
+        ldap.ScopeWholeSubtree, 
+        ldap.NeverDerefAliases, 0, 0, false,
+        "(memberUid=" + u.UID + ")", // Filter by UID membership
+        []string{"dn"}, 
+        nil,
+    )
+    
+    groups, err := l.Search(searchReq)
+    if err != nil && !ldap.IsErrorWithCode(err, ldap.LDAPResultNoSuchObject) {
+        return fmt.Errorf("Group members search failed: %v", err)
+    }
+
+    for _, entry := range groups.Entries {
+        modReq := ldap.NewModifyRequest(entry.DN, nil)
+        modReq.Delete("memberUid", []string{u.UID})
+        if err := l.Modify(modReq); err != nil && 
+                  !ldap.IsErrorWithCode(err, ldap.LDAPResultNoSuchAttribute) {
+            log.Printf("Warning: Failed to remove from group %s: %v", entry.DN, err)
+        }
+    }
+
+    // Delete user entry
+    delReq := ldap.NewDelRequest(u.DN, nil)
+    if err := l.Del(delReq); err != nil && 
+              !ldap.IsErrorWithCode(err, ldap.LDAPResultNoSuchObject) {
+        return fmt.Errorf("user deletion failed: %v", err)
+    }
+
+    return nil
+}
+
+func delKerberosPrincipal(login string) error {
+    cmd := exec.Command("kadmin.local", "-q", fmt.Sprintf("delprinc -force %s", login))
+
+    output, err := cmd.CombinedOutput()
+    if err != nil {
+        return fmt.Errorf("Fail to delete Kerberos principal: %v\nOutput: %s", err, output)
+    }
+
+    return nil
+}
diff --git a/cmd/user/mod.go b/cmd/user/mod.go
index 368802c..632d471 100644
--- a/cmd/user/mod.go
+++ b/cmd/user/mod.go
@@ -91,7 +91,7 @@ func modifyUserFunc(cmd *cobra.Command, args []string) error {
         userPasswordNew = "auto"
     } else if unblock {
         userShellNew    = "/bin/bash"
-        userStatusNew   = "Free!"
+        userStatusNew   = "Active"
         userPasswordNew = "auto"
     }
 
@@ -109,7 +109,7 @@ func modifyUserFunc(cmd *cobra.Command, args []string) error {
 
     if cmd.Flags().Changed("password") || block || unblock {
         if userPasswordNew == "auto" {
-            userPasswordNew = generatePassword()
+            userPasswordNew = genPassword()
             userMod.Password = "[auto-generated]"
         } else {
             userMod.Password = "[explicitly set]"
@@ -120,15 +120,7 @@ func modifyUserFunc(cmd *cobra.Command, args []string) error {
 
     fmt.Println(userMod.ToString())
 
-    if !confirm {
-        fmt.Print("Proceed with user update? [y/N] ")
-        reader := bufio.NewReader(os.Stdin)
-        response, _ := reader.ReadString('\n')
-        if strings.TrimSpace(strings.ToLower(response)) != "y" {
-            fmt.Fprintln(os.Stderr, "Aborted.")
-            os.Exit(1)
-        }
-    }
+    confirmationPrompt(confirm, "update")
 
     fmt.Println("Done!")
 
@@ -137,7 +129,7 @@ func modifyUserFunc(cmd *cobra.Command, args []string) error {
     return nil
 }
 
-func generatePassword() string {
+func genPassword() string {
     const charset = "@*()=+[];,.?123456789abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ"
     b := make([]byte, 20)
     if _, err := rand.Read(b); err != nil {
@@ -149,3 +141,17 @@ func generatePassword() string {
     return string(b)
 }
 
+func confirmationPrompt(confirm bool, operation string) {
+    if !confirm {
+        fmt.Printf("Proceed with user %v? [y/N] ", operation)
+        reader := bufio.NewReader(os.Stdin)
+        response, _ := reader.ReadString('\n')
+        if strings.TrimSpace(strings.ToLower(response)) != "y" {
+            fmt.Fprintln(os.Stderr, "Aborted.")
+            os.Exit(1)
+        }
+    }
+}
+
+
+
diff --git a/cmd/user/show.go b/cmd/user/show.go
index 0e42af1..8be66eb 100644
--- a/cmd/user/show.go
+++ b/cmd/user/show.go
@@ -3,7 +3,8 @@ package user
 import (
 	"os"
 	"fmt"
-	"log"
+	"sort"
+	"strconv"
 	"strings"
 
 	"github.com/spf13/cobra"
@@ -11,6 +12,11 @@ import (
 	"gitlab.c3sl.ufpr.br/tss24/useradm/model"
 )
 
+const (
+    NUM_GECOS_FIELDS = 9
+    PASSWD_PATH = "/etc/ldapscripts/ldapscripts.passwd"
+)
+
 var ShowCmd = &cobra.Command{
 	Use:   "show",
 	Short: "Search for users and show info",
@@ -23,7 +29,7 @@ func init() {
     ShowCmd.Flags().StringP("name", "n", "", "Search by user name (case sensitive)")
     ShowCmd.Flags().StringP("login", "l", "", "Search by login")
     ShowCmd.Flags().StringP("group", "g", "", "Search by user base group")
-    ShowCmd.Flags().StringP("status", "a", "", "Search by user status (Free/Blocked)")
+    ShowCmd.Flags().StringP("status", "s", "", "Search by user status (Active/Blocked)")
     ShowCmd.Flags().StringP("homedir", "d", "", "Search by user homedir")
 
     // At least one is required!
@@ -32,31 +38,19 @@ func init() {
 
 func searchUser(cmd *cobra.Command, args []string) error {
     grr, err := cmd.Flags().GetString("grr")
-    if err != nil {
-        return err
-    }
+    if err != nil { return err }
     name, err := cmd.Flags().GetString("name")
-    if err != nil {
-        return err
-    }
+    if err != nil { return err }
     login, err := cmd.Flags().GetString("login")
-    if err != nil {
-        return err
-    }
+    if err != nil { return err }
     group, err := cmd.Flags().GetString("group")
-    if err != nil {
-        return err
-    }
+    if err != nil { return err }
     homedir, err := cmd.Flags().GetString("homedir")
-    if err != nil {
-        return err
-    }
+    if err != nil { return err }
     status, err := cmd.Flags().GetString("status")
-    if err != nil {
-        return err
-    }
-
-    users := getUsers()
+    if err != nil { return err }
+    users, err := getUsers()
+    if err != nil { return err }
 
     filtered := Filter(users, func(u model.User) bool {
         return (grr     == "" || strings.Contains(u.GRR, grr)) &&
@@ -85,25 +79,16 @@ func Filter[T any](slice []T, predicate func(T) bool) []T {
     return result
 }
 
-func getUsers() []model.User {
-    // Connect to the LDAP server
-    l, err := ldap.DialURL("ldapi:///")
-    if err != nil {
-        log.Fatal(err)
-    }
-    defer l.Close()
+func getUsers() ([]model.User, error) {
 
-    // Get admin credentials
-    password, err := getLDAPPassword("/etc/ldapscripts/ldapscripts.passwd")
-    if err != nil {
-        log.Fatalf("Failed to read LDAP password: %v", err)
-    }
+    var users []model.User
 
-    // Bind using admin credentials
-    err = l.Bind("cn=admin,dc=c3local", password)
+    // Connect
+    l, err := connLDAP() 
     if err != nil {
-        log.Fatalf("Failed to bind with credentials: %v", err)
+        return users, err
     }
+    defer l.Close()
 
     // Create the LDAP search request
     searchRequest := ldap.NewSearchRequest(
@@ -114,23 +99,28 @@ func getUsers() []model.User {
         0,                           // time limit
         false,                       // types only
         "(objectClass=posixAccount)",// filter
-        []string{"dn", "uid", "cn", "loginShell", "uidNumber", "gidNumber", "homeDirectory", "gecos"}, // attributes to return
+        []string{"dn", "uid", "cn", "loginShell", "uidNumber", 
+                       "gidNumber", "homeDirectory", "gecos"}, // attributes to return
         nil,
     )
 
     // Perform the search
     sr, err := l.Search(searchRequest)
     if err != nil {
-        log.Fatalf("Failed to make LDAP search: %v", err)
+        err = fmt.Errorf("Failed to fetch users from LDAP: %v", err)
+        return nil, err
     }
 
-    var users []model.User
+    // Get all the groups and ids
+    groups, err := getGroups()
+    if err != nil {
+        err = fmt.Errorf("Failed to fetch groups and gids from LDAP: %v", err) 
+        return nil, err
+    }
 
     // Iterate over the search results
     for _, entry := range sr.Entries {
-        gecos := entry.GetAttributeValue("gecos")
         shell := entry.GetAttributeValue("loginShell")
-        gidNumber := entry.GetAttributeValue("gidNumber")
 
         user := model.User{
             DN:        entry.DN,
@@ -138,17 +128,20 @@ func getUsers() []model.User {
             Name:      entry.GetAttributeValue("cn"),
             Shell:     shell,
             UIDNumber: entry.GetAttributeValue("uidNumber"),
-            GIDNumber: gidNumber,
+            GIDNumber: entry.GetAttributeValue("gidNumber"),
             Homedir:   entry.GetAttributeValue("homeDirectory"),
-            Gecos:     gecos,
-            Status:    ifThenElse(shell == "/bin/bash", "Free", "Blocked"),
+            Gecos:     entry.GetAttributeValue("gecos"),
+            Status:    ifThenElse(shell == "/bin/bash", "Active", "Blocked"),
         }
 
-        // Query for the group name using gidNumber
-        groupName, err := getGroupNameByGID(l, gidNumber)
-        if err != nil {
-            log.Printf("Warning: Failed to find group name for gidNumber %s: %v", gidNumber, err)
-            user.GID = "Unknown"
+        user.GID = groups[user.GIDNumber]
+
+        gidNumber := user.GIDNumber
+
+        // Safe assignment
+        groupName, exists := groups[gidNumber]
+        if !exists {
+            fmt.Printf("WARNING: no group found for GIDNumber %s, user %s. Continuing...", gidNumber, user.UID)
         } else {
             user.GID = groupName
         }
@@ -156,13 +149,41 @@ func getUsers() []model.User {
         user  = parseGecos(user)
         users = append(users, user)
     }
-    return users
+
+    return users, nil
+}
+
+func connLDAP() (*ldap.Conn, error) {
+    // Connect to the LDAP server
+    l, err := ldap.DialURL("ldapi:///")
+    if err != nil {
+        err = fmt.Errorf("Failed to connect to LDAP: %v", err)
+        return nil, err
+    }
+
+    // Get admin credentials
+    password, err := getLDAPPassword(PASSWD_PATH)
+    if err != nil {
+        err = fmt.Errorf("Failed to read LDAP password: %v", err)
+        return nil, err
+    }
+
+    // Bind using admin credentials
+    err = l.Bind("cn=admin,dc=c3local", password)
+    if err != nil {
+        l.Close()
+        err = fmt.Errorf("Failed to bind with credentials: %v", err)
+        return nil, err
+    }
+    
+    return l, nil
 }
 
 func getLDAPPassword(path string) (string, error) {
     passwd, err := os.ReadFile(path)
     if err != nil {
-        return "", fmt.Errorf("Error reading password file: %w", err)
+        err = fmt.Errorf("Error reading password file: %w", err)
+        return "", err
     }
 
     password := strings.TrimSpace(string(passwd))
@@ -176,17 +197,19 @@ func getLDAPPassword(path string) (string, error) {
 // 2. Professor responsavel
 // 3. Curso/minicurso do usuario (ate para temporarios) 
 // 4. Validade da conta (data maxima para delecao)
-// 5. Status da conta (Blocked/Free)
+// 5. Status da conta (Blocked/Active)
 // 6. Tipo de login (ini, first, last)
-// 7. to be continued...
+// 7. Webdir do usuario
+// 8. Nobackup do usuario
+// 9. to be continued...
 func parseGecos(user model.User) model.User {
-    result := [7]string{0: "_"}
+    result := [NUM_GECOS_FIELDS]string{0: "_"}
     for i := range result {
         result[i] = "_"
     }    
     parts := strings.Split(user.Gecos, ",")
 
-    for i := 0; i < len(parts) && i < 7; i++ {
+    for i := 0; i < len(parts) && i < NUM_GECOS_FIELDS; i++ {
         if strings.TrimSpace(parts[i]) != "" {
             result[i] = parts[i]
         }
@@ -195,37 +218,111 @@ func parseGecos(user model.User) model.User {
     user.GRR    = result[1]
     user.Resp   = result[2]
     user.Course = result[3]
-    user.Expiry = result[4]
     if user.Status == "_"{
         user.Status = result[5]
     }
+    user.Expiry = result[4]
     user.Ltype  = result[6]
+    user.Webdir = result[7]
+    user.Nobackup = result[8]
 
     return user
 }
 
-// Helper function to get group name by gidNumber
-func getGroupNameByGID(l *ldap.Conn, gidNumber string) (string, error) {
+
+func getGroups() (map[string]string, error) {
+
+    groupMap := make(map[string]string)
+
+    l, err := connLDAP() 
+    if err != nil {
+        return groupMap, err
+    }
+    defer l.Close()
+
     searchRequest := ldap.NewSearchRequest(
-        "dc=c3local",                // search base
-        ldap.ScopeWholeSubtree,      // scope
-        ldap.NeverDerefAliases,      // aliases
-        1,                           // size limit (one result expected)
-        0,                           // time limit
-        false,                       // types only
-        fmt.Sprintf("(&(objectClass=posixGroup)(gidNumber=%s))", gidNumber), // filter by gidNumber
-        []string{"cn"},              // retrieve the group name (cn)
+        "dc=c3local",           
+        ldap.ScopeWholeSubtree, 
+        ldap.NeverDerefAliases, 
+        0, 0,                      
+        false,                  
+        "(&(objectClass=posixGroup))", 
+        []string{"gidNumber", "cn"},   
+        nil,
+    )
+
+    sr, err := l.Search(searchRequest)
+    if err != nil {
+        return nil, fmt.Errorf("LDAP search failed: %w", err)
+    }
+
+    for _, entry := range sr.Entries {
+        gid := entry.GetAttributeValue("gidNumber")
+        cn := entry.GetAttributeValue("cn")
+        if gid != "" && cn != "" {
+            groupMap[gid] = cn
+        }
+    }
+
+    return groupMap, nil
+}
+
+func getUIDs() ([]int, error) {
+
+    var uidNumbers []int
+
+    l, err := connLDAP() 
+    if err != nil {
+        return uidNumbers, err
+    }
+    defer l.Close()
+
+    searchRequest := ldap.NewSearchRequest(
+        "dc=c3local",
+        ldap.ScopeWholeSubtree,
+        ldap.NeverDerefAliases,
+        0, 0,
+        false,
+        "(&(objectClass=posixAccount))",
+        []string{"uidNumber"},
         nil,
     )
 
     sr, err := l.Search(searchRequest)
     if err != nil {
-        return "", fmt.Errorf("LDAP search failed: %w", err)
+        return nil, fmt.Errorf("LDAP search failed: %w", err)
     }
 
-    if len(sr.Entries) == 0 {
-        return "", fmt.Errorf("no group found with gidNumber %s", gidNumber)
+    for _, entry := range sr.Entries {
+        uidStr := entry.GetAttributeValue("uidNumber")
+        if uidStr != "" {
+            uid, err := strconv.Atoi(uidStr)
+            if err != nil {
+                return nil, fmt.Errorf("invalid UID number: %s", uidStr)
+            }
+            uidNumbers = append(uidNumbers, uid)
+        }
     }
 
-    return sr.Entries[0].GetAttributeValue("cn"), nil
+    sort.Ints(uidNumbers)
+
+    return uidNumbers, nil
+}
+
+func loginExists(users []model.User, login string) bool {
+    return exists(users, func(u model.User) bool { return u.UID == login })
+}
+
+func grrExists(users []model.User, grr string) bool {
+    return exists(users, func(u model.User) bool { return u.GRR == grr }) ||
+           exists(users, func(u model.User) bool { return u.GRR == "GRR" + grr }) 
+}
+
+func exists[T any](slice []T, predicate func(T) bool) bool {
+    for _, item := range slice {
+        if predicate(item) {
+            return true
+        }
+    }
+    return false
 }
diff --git a/model/user.go b/model/user.go
index 1b6cbac..901c8a4 100644
--- a/model/user.go
+++ b/model/user.go
@@ -4,21 +4,22 @@ import "fmt"
 
 type User struct {
     DN        string  
-	GRR       string
-	UID       string
-	GID       string
-	Name      string
+    GRR       string
+    UID       string
+    GID       string
+    Name      string
     Resp      string
-	Ltype     string
+    Ltype     string
     Gecos     string
-	Shell     string
+    Shell     string
     Course    string
     Status    string
-	Webdir    string
+    Webdir    string
     Expiry    string
-	Homedir   string
-	Password  string
-	GIDNumber string
+    Homedir   string
+    Password  string
+    Nobackup  string
+    GIDNumber string
     UIDNumber string
 }
 
@@ -29,11 +30,38 @@ func (u *User) ToString() string {
 	GRR:     %s
 	Group:   %s
 	Shell:   %s
-	HomeDir: %s
+	Home:    %s
 	Webdir:  %s
+	Nobkp:   %s
 	Status:  %s
 	Resp:    %s
 	Course:  %s
 	Expiry:  %s`, 
-		u.Name, u.UID, u.GRR, u.GID, u.Shell, u.Homedir, u.Webdir, u.Status, u.Resp, u.Course, u.Expiry)
+		u.Name, u.UID, u.GRR, u.GID, u.Shell, u.Homedir, u.Webdir, 
+		u.Nobackup, u.Status, u.Resp, u.Course, u.Expiry)
+}
+
+func (u *User) FullToString() string {
+    return fmt.Sprintf(`User:
+	DN:        %s  
+	GRR:       %s
+	UID:       %s
+	GID:       %s
+	Name:      %s
+	Resp:      %s
+	Ltype:     %s
+	Gecos:     %s
+	Shell:     %s
+	Course:    %s
+	Status:    %s
+	Webdir:    %s
+	Expiry:    %s
+	Homedir:   %s
+	Password:  %s
+	Nobackup:  %s
+	GIDNumber: %s
+	UIDNumber: %s`,
+		u.DN, u.GRR, u.UID, u.GID, u.Name, u.Resp, u.Ltype, u.Gecos, 
+		u.Shell,u.Course, u.Status, u.Webdir, u.Expiry, u.Homedir, 
+		u.Password, u.Nobackup, u.GIDNumber, u.UIDNumber)
 }
-- 
GitLab