From 5f812d79843d53d12c21b2ab1d325b8eab5a140b Mon Sep 17 00:00:00 2001
From: Theo S Schult <theo.sschult@gmail.com>
Date: Wed, 19 Feb 2025 19:06:19 -0300
Subject: [PATCH] Finish delete prototype and debug/fix create

---
 cmd/user.go        |   2 +-
 cmd/user/create.go | 155 +++++++++++++++++++++++----------------------
 cmd/user/delete.go | 119 +++++++++++++++++++++++++++++++++-
 cmd/user/show.go   |  24 ++++---
 4 files changed, 213 insertions(+), 87 deletions(-)

diff --git a/cmd/user.go b/cmd/user.go
index 9ca388f..58d77b1 100644
--- a/cmd/user.go
+++ b/cmd/user.go
@@ -12,7 +12,7 @@ var userCmd = &cobra.Command{
 
 func init() {
 	userCmd.AddCommand(user.CreateUserCmd)
-	userCmd.AddCommand(user.DeleteCmd)
+	userCmd.AddCommand(user.DeleteUserCmd)
 	userCmd.AddCommand(user.ModCmd)
 	userCmd.AddCommand(user.ShowCmd)
 }
diff --git a/cmd/user/create.go b/cmd/user/create.go
index 56f936c..4937843 100644
--- a/cmd/user/create.go
+++ b/cmd/user/create.go
@@ -1,26 +1,27 @@
 package user
 
 import (
+	"os"
 	"fmt"
 	"log"
-	"os"
-	"os/exec"
-	"path/filepath"
-	"regexp"
 	"sort"
+	"time"
+	"regexp"
+	"os/exec"
 	"strconv"
 	"strings"
-	"time"
+	"path/filepath"
 
-	"github.com/go-ldap/ldap/v3"
 	"github.com/spf13/cobra"
+	"github.com/go-ldap/ldap/v3"
 	"gitlab.c3sl.ufpr.br/tss24/useradm/model"
 )
 
 const (
     MAIL_ALIAS_FILE = "/etc/aliases"
-    DEF_PERMISSION  = 0700
-    BLK_PERMISSION  = 0000
+    DEF_PERMISSION  = "0700"
+    BLK_PERMISSION  = "0000"
+    HTML_BASE_PERM  = "0755"
     MAX_VARIANCE    = 50
     MIN_UID         = 1000
     MAX_UID         = 6000
@@ -36,7 +37,7 @@ func init() {
 	// Possible Flags
 	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("path", "w", "", "Full path to webdir, /home/html/inf/login if empty")
 	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")
@@ -70,8 +71,8 @@ func createUserFunc(cmd *cobra.Command, args []string) error {
     }()
 
     // Prints info for confirmation
-	fmt.Printf("%v\n", usr.FullToString())
-	//fmt.Printf("%v\n	Passwd:  %v\n\n", usr.ToString(), usr.Password)
+	//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
@@ -91,7 +92,7 @@ func createUserFunc(cmd *cobra.Command, args []string) error {
     err = createUserDirs(usr)
     if err != nil { return err }
 
-	fmt.Println("Done!")
+	fmt.Println("User created!")
     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,
@@ -201,7 +202,7 @@ func createNewUserModel(cmd *cobra.Command) (model.User, bool, error) {
 		Webdir:   userWebdir,
 		Homedir:  userHomedir,
 		Nobackup: userNobackup,
-		Password: ifThenElse(userPassword != "", "[set]", "[auto-generate]"),
+		Password: ifThenElse(userPassword != "", userPassword, "[auto-generate]"),
 	}
 
     u = genGecos(u)
@@ -245,7 +246,7 @@ func genNobackupPath(login string, group string, nobkp string) string {
 }
 
 func genGecos(user model.User) model.User {
-    gecos := user.UID + ","
+    gecos := user.Name + ","
     gecos += user.GRR + ","
     gecos += user.Resp + ","
     gecos += user.Course + ","
@@ -462,26 +463,43 @@ func mailAliasExists(alias string) bool {
     return cmd.Run() == nil
 } 
 
-func getNewUIDNumber() (string, error) {
-    var avail int
-
+func getNewUIDNumberLinear() (string, error) {
     uids, err := getUIDs()
+    if err != nil {
+        return "", err
+    }
 
-    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
-        })
+    candidate := MIN_UID
+    for _, uid := range uids {
+        if uid == candidate { // Check if taken
+            candidate++
+        } else if uid > candidate { // Found a gap
+            break
+        }
     }
 
-    if avail > MAX_UID { 
-        err = fmt.Errorf("No more available UIDNumbers")
-        return "", err 
+    if candidate > MAX_UID {
+        return "", fmt.Errorf("No more available UID numbers")
     }
+    return strconv.Itoa(candidate), nil
+}
 
-    uid := strconv.Itoa(avail)
-    return uid, nil
+// Finds next available uidNumber
+func getNewUIDNumber() (string, error) {
+    uids, err := getUIDs()
+    if err != nil {
+        return "", err
+    }
+    for candidate := MIN_UID; candidate <= MAX_UID; candidate++ {
+        i := sort.Search(len(uids), func(j int) bool {
+            return uids[j] >= candidate
+        })
+        if i == len(uids) || uids[i] != candidate {
+            // Candidate UID is not in the list, so this is available.
+            return strconv.Itoa(candidate), nil
+        }
+    }
+    return "", fmt.Errorf("no more available UID numbers")
 }
 
 func addUserLDAP(u model.User) error {
@@ -539,9 +557,9 @@ func createUserDirs(u model.User) error {
     success := false
     defer func() {
         if !success {
-            fmt.Println("Error found! Cleaning up...")
-            _ = DeleteHome(u.Nobackup)
-            _ = DeleteHome(u.Homedir)
+            fmt.Println("Error found creating dirs, cleaning up...")
+            _ = os.RemoveAll(u.Nobackup)
+            _ = os.RemoveAll(u.Homedir)
         }
     }()
 
@@ -558,77 +576,67 @@ func createUserDirs(u model.User) error {
     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
+    perm := DEF_PERMISSION
     if u.Status == "Blocked" {
         perm = BLK_PERMISSION
     }
 
 	// Create directory
-	if err := os.MkdirAll(homeDir, perm); err != nil {
+	cmd := exec.Command("mkdir", "-p", homeDir)
+    cmd.Stdout = nil
+    cmd.Stderr = nil
+	if err := cmd.Run(); 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
+	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)
 	}
 
-    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 {
+    cmd = exec.Command("chmod", perm, homeDir)
+	if err := cmd.Run(); err != nil {
 		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 nil
 }
 
 func createWeb(u model.User) error {
-    var perm os.FileMode
     success := false
 
-    perm = DEF_PERMISSION
+    perm := DEF_PERMISSION
     if u.Status == "Blocked" {
         perm = BLK_PERMISSION
     }
 
     defer func() {
         if !success {
-            _ = DeleteHome(u.Webdir)
+            _ = os.RemoveAll(u.Webdir)
         }
     }()
 
 	// Create directory
-	if err := os.MkdirAll(u.Webdir, perm); err != nil {
+	cmd := exec.Command("mkdir", "-p", u.Webdir)
+    cmd.Stdout = nil
+    cmd.Stderr = nil
+	if err :=  cmd.Run(); 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
+	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)
 	}
@@ -639,22 +647,21 @@ func createWeb(u model.User) error {
 		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 permissions
+    cmd = exec.Command("chmod", perm, u.Webdir)
+	if err := cmd.Run(); err != nil {
+		return fmt.Errorf("Failed to set permissions: %w", err)
+	}
+
 	// Change ownership
-	if err := os.Chown(u.Webdir, uid, gid); err != nil {
+    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)
 	}
 
-	// Change permissions
-	if err := os.Chmod(u.Webdir, perm); err != nil {
-		return fmt.Errorf("Failed to set permissions: %w", err)
+	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)
 	}
 
     success = true
diff --git a/cmd/user/delete.go b/cmd/user/delete.go
index b9e4bfa..2157184 100644
--- a/cmd/user/delete.go
+++ b/cmd/user/delete.go
@@ -1,18 +1,103 @@
 package user
 
 import (
+	"os"
 	"fmt"
 	"log"
+	"time"
 	"os/exec"
+	"strconv"
+	"path/filepath"
 
-	"github.com/go-ldap/ldap/v3"
 	"github.com/spf13/cobra"
+	"github.com/go-ldap/ldap/v3"
 	"gitlab.c3sl.ufpr.br/tss24/useradm/model"
 )
 
-var DeleteCmd = &cobra.Command{
+
+var (
+    ANO = strconv.Itoa(time.Now().Year())
+    NO_BKP_TRASH = "/nobackup/contas_removidas/" + ANO
+    HOME_TRASH =  "/home/contas_removidas/" + ANO
+    WEB_TRASH =  "/home/contas_removidas/html/" + ANO
+)
+
+var DeleteUserCmd = &cobra.Command{
 	Use:   "delete",
 	Short: "Delete a user",
+    RunE:  deleteUserFunc,
+}
+
+func init() {
+    DeleteUserCmd.Flags().StringP("login", "l", "", "User login for removal")
+    DeleteUserCmd.Flags().BoolP("confirm", "y", false, "Skip confirmation prompt")
+
+    DeleteUserCmd.MarkFlagRequired("login")
+}
+
+func deleteUserFunc(cmd *cobra.Command, args []string) error {
+    success := false
+    userLogin, err := cmd.Flags().GetString("login")
+    if err != nil { return err }
+    confirm, err := cmd.Flags().GetBool("confirm")
+    if err != nil { return err }
+
+    u, err := locateUser(userLogin)
+    if err != nil { return err }
+
+    defer func() {
+        if !success {
+            log.Println("Found error, rolling back dirs...")
+            _ = moveAndChown(filepath.Join(NO_BKP_TRASH, filepath.Base(u.Nobackup)),
+                u.Nobackup, u.UID, u.GID)
+            _ = moveAndChown(filepath.Join(HOME_TRASH, filepath.Base(u.Homedir)),
+                u.Homedir, u.UID, u.GID)
+            _ = moveAndChown(filepath.Join(WEB_TRASH, filepath.Base(u.Webdir)),
+                u.Webdir, u.UID, u.GID)
+        }
+    }()
+
+    fmt.Printf("Found %v\n\n", u.ToString())
+
+    confirmationPrompt(confirm, "removal")
+
+    err = removeDirs(u)
+    if err != nil { return err }
+
+    err = delUserLDAP(u)
+    if err != nil { return err }
+
+    fmt.Printf("\nUser removed!\n")
+    success = true
+    return nil
+}
+
+func locateUser(login string) (model.User, error) {
+    var u model.User
+    users, err := getUsers()
+    if !loginExists(users, login) {
+        return u, fmt.Errorf("Failed to find login in LDAP database: %v", err)
+    }
+    filter := searchUser(users, login, "", "", "", "", "")
+    if len(filter) != 1 {
+        return u, fmt.Errorf(`More than one user matched the login given.
+search made: "useradm user show -l %v"`, login)
+    }
+    u = filter[0]
+    return u, nil
+}
+
+func removeDirs(u model.User) error {
+    err := moveAndChown(u.Homedir, HOME_TRASH, "nobody", "nogroup")
+    if err != nil { return err }
+
+    err = moveAndChown(u.Nobackup, NO_BKP_TRASH, "nobody", "nogroup")
+    if err != nil { return err }
+
+    err = moveAndChown(u.Webdir, WEB_TRASH, "nobody", "nogroup")
+    if err != nil { return err }
+
+    return nil
 }
 
 func delUserLDAP(u model.User) error {
@@ -66,3 +151,33 @@ func delKerberosPrincipal(login string) error {
 
     return nil
 }
+
+func moveAndChown(orig, dest, owner, group string) error {
+    // Check if orig exists
+    if _, err := os.Stat(orig); err != nil {
+        log.Printf("Directory %v not found so not moved\n", orig)
+        return nil
+    }
+
+	// Construct destination path
+	destPath := filepath.Join(dest, filepath.Base(orig))
+
+    if _, err := os.Stat(destPath); err == nil {
+        return fmt.Errorf("Directory %v already exists\n", destPath)
+    }
+
+	// Move directory using shell command
+	cmd := exec.Command("mv", orig, destPath)
+	if output, err := cmd.CombinedOutput(); err != nil {
+		return fmt.Errorf("Failed to move dirs: %w\nOutput: %v", err, output)
+	}
+
+	// Recursive chown using shell command
+	cmd = exec.Command("chown", "-R", owner+":"+group, destPath)
+	if output, err := cmd.CombinedOutput(); err != nil {
+		return fmt.Errorf("Failed to set owner/group: %w\nOutput: %v", err, output)
+	}
+
+	return nil
+}
+
diff --git a/cmd/user/show.go b/cmd/user/show.go
index 8be66eb..0fbd4ac 100644
--- a/cmd/user/show.go
+++ b/cmd/user/show.go
@@ -20,7 +20,7 @@ const (
 var ShowCmd = &cobra.Command{
 	Use:   "show",
 	Short: "Search for users and show info",
-    RunE:  searchUser,
+    RunE:  searchUserFunc,
 }
 
 func init() {
@@ -36,7 +36,7 @@ func init() {
     ShowCmd.MarkFlagsOneRequired("grr", "name", "login", "group", "homedir", "status")
 }
 
-func searchUser(cmd *cobra.Command, args []string) error {
+func searchUserFunc(cmd *cobra.Command, args []string) error {
     grr, err := cmd.Flags().GetString("grr")
     if err != nil { return err }
     name, err := cmd.Flags().GetString("name")
@@ -52,14 +52,7 @@ func searchUser(cmd *cobra.Command, args []string) error {
     users, err := getUsers()
     if err != nil { return err }
 
-    filtered := Filter(users, func(u model.User) bool {
-        return (grr     == "" || strings.Contains(u.GRR, grr)) &&
-               (name    == "" || strings.Contains(u.Name, name)) &&
-               (login   == "" || strings.Contains(u.UID, login)) &&
-               (group   == "" || strings.Contains(u.GID, group)) &&
-               (status  == "" || strings.Contains(u.Status, status)) &&
-               (homedir == "" || strings.Contains(u.Homedir, homedir))
-    })
+    filtered := searchUser(users, login, group, name, grr, status, homedir)
 
     for i := range filtered {
         fmt.Printf("%v\n\n",filtered[i].ToString())
@@ -68,6 +61,17 @@ func searchUser(cmd *cobra.Command, args []string) error {
     return nil
 }
 
+func searchUser(users []model.User, l, g, n, r, s, h string) []model.User {
+    return Filter(users, func(u model.User) bool {
+        return (r == "" || strings.Contains(u.GRR, r)) &&
+               (n == "" || strings.Contains(u.Name, n)) &&
+               (l == "" || strings.Contains(u.UID, l)) &&
+               (g == "" || strings.Contains(u.GID, g)) &&
+               (s == "" || strings.Contains(u.Status, s)) &&
+               (h == "" || strings.Contains(u.Homedir, h))
+    })
+}
+
 // Generic function for filtering data types, in this case the model User
 func Filter[T any](slice []T, predicate func(T) bool) []T {
     var result []T
-- 
GitLab