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