From 48df4c2a360326bc8e92ad70a97259e4bbb0709a Mon Sep 17 00:00:00 2001 From: Theo <tss24@inf.ufpr.br> Date: Mon, 31 Mar 2025 22:41:39 -0300 Subject: [PATCH] Start group subcommand implementation --- cmd/bulk.go | 2 +- cmd/group.go | 2 +- cmd/group/create.go | 60 +++++++++++++++++++----------- cmd/group/delete.go | 51 ++++++++++++++++++++++++-- cmd/user/block.go | 28 ++++++++++---- cmd/user/create.go | 10 +++-- cmd/user/group.go | 62 +++++++++++++++++++++++++++++++ cmd/user/mod.go | 2 +- cmd/user/remove.go | 15 ++++---- cmd/user/show.go | 4 +- cmd/user/temp.go | 3 +- cmd/user/unblock.go | 20 ++++++---- model/ldap.go | 89 +++++++++++++++++++++++++++++++++++++++++++-- model/user.go | 24 ++++++++++++ utils/utils.go | 2 +- 15 files changed, 310 insertions(+), 64 deletions(-) create mode 100644 cmd/user/group.go diff --git a/cmd/bulk.go b/cmd/bulk.go index 3d75a75..0dde21e 100644 --- a/cmd/bulk.go +++ b/cmd/bulk.go @@ -124,7 +124,7 @@ func bulkCreate(cmd *cobra.Command, args []string) error { if !opts.Confirm { fmt.Printf("Ready to create: %v\n", u.FullToString()) } - utils.ConfirmationPrompt(opts.Confirm, "creation") + utils.ConfirmationPrompt(opts.Confirm, "user creation") err = u.Create(l) if err != nil { return err diff --git a/cmd/group.go b/cmd/group.go index 49a7be1..5e41212 100644 --- a/cmd/group.go +++ b/cmd/group.go @@ -7,7 +7,7 @@ import ( var groupCmd = &cobra.Command{ Use: "group", - Short: "User subcommand", + Short: "Group subcommand", } func init() { diff --git a/cmd/group/create.go b/cmd/group/create.go index 1f2a9a6..25e2fa5 100644 --- a/cmd/group/create.go +++ b/cmd/group/create.go @@ -1,48 +1,64 @@ package group import ( - "bufio" "fmt" - "os" - "strings" "github.com/spf13/cobra" + "gitlab.c3sl.ufpr.br/tss24/useradm/model" + "gitlab.c3sl.ufpr.br/tss24/useradm/utils" ) var CreateCmd = &cobra.Command{ - Use: "create", + Use: "create [groupname]", Short: "Create new group", - RunE: createGroupFunc, + Args: cobra.ExactArgs(1), + RunE: createGroupCmd, } func init() { - CreateCmd.Flags().StringP("groupname", "e", "", "Group name (use quotes for spaces)") CreateCmd.Flags().BoolP("confirm", "y", false, "Skip confirmation prompt") - - CreateCmd.MarkFlagRequired("groupname") } -func createGroupFunc(cmd *cobra.Command, args []string) error { - groupName, err := cmd.Flags().GetString("groupname") +func createGroupCmd(cmd *cobra.Command, args []string) error { + var opts model.Opts + + err := opts.RetrieveOpts(cmd) + if err != nil { + return err + } + + l, err := model.ConnLDAP() + if err != nil { + return err + } + defer l.Close() + + groups, err := model.GetAllGroupsLDAP(l) if err != nil { return err } - confirm, err := cmd.Flags().GetBool("confirm") + + // there has to be a not found error + _, err = utils.GetGIDNumFromGID(groups, args[0]) + if err == nil { + return fmt.Errorf("Groupname already exists") + } + + GIDNumber, err := model.GetNewGIDNumber(l) if err != nil { return err } - fmt.Printf("Groupname: %s\n", groupName) - - if !confirm { - fmt.Print("Proceed with group creation? [y/N] ") - reader := bufio.NewReader(os.Stdin) - response, _ := reader.ReadString('\n') - if strings.TrimSpace(strings.ToLower(response)) != "y" { - fmt.Println("Aborted.") - os.Exit(1) - } + + fmt.Printf("Groupname: %v\n", args[0]) + fmt.Printf("gidNumber: %v\n", GIDNumber) + utils.ConfirmationPrompt(opts.Confirm, "group creation") + + err = model.LDAPCreateGroup(l, args[0], GIDNumber) + if err != nil { + return err } - fmt.Println("Done!") + fmt.Println("Group Created!") + return nil } diff --git a/cmd/group/delete.go b/cmd/group/delete.go index 7fdbd73..f021b47 100644 --- a/cmd/group/delete.go +++ b/cmd/group/delete.go @@ -1,13 +1,58 @@ package group -import "github.com/spf13/cobra" +import ( + "fmt" + + "github.com/spf13/cobra" + "gitlab.c3sl.ufpr.br/tss24/useradm/model" + "gitlab.c3sl.ufpr.br/tss24/useradm/utils" +) var DeleteCmd = &cobra.Command{ Use: "delete", Short: "Delete a group", - RunE: deleteRun, + Args: cobra.ExactArgs(1), + RunE: deleteCmd, +} + +func init() { + DeleteCmd.Flags().BoolP("confirm", "y", false, "Skip confirmation prompt") } -func deleteRun(cmd *cobra.Command, args []string) error { +func deleteCmd(cmd *cobra.Command, args []string) error { + var opts model.Opts + + err := opts.RetrieveOpts(cmd) + if err != nil { + return err + } + + l, err := model.ConnLDAP() + if err != nil { + return err + } + defer l.Close() + + groups, err := model.GetAllGroupsLDAP(l) + if err != nil { + return err + } + + gidNumber, err := utils.GetGIDNumFromGID(groups, args[0]) + if err != nil { + return fmt.Errorf("Groupname not found") + } + + fmt.Printf("Groupname: %v\n", args[0]) + fmt.Printf("gidNumber: %v\n", gidNumber) + utils.ConfirmationPrompt(opts.Confirm, "group deletion") + + err = model.LDAPDeleteGroup(l, args[0]) + if err != nil { + return err + } + + fmt.Println("Group deleted!") + return nil } diff --git a/cmd/user/block.go b/cmd/user/block.go index e148992..c251bbb 100644 --- a/cmd/user/block.go +++ b/cmd/user/block.go @@ -11,27 +11,41 @@ import ( var BlockCmd = &cobra.Command{ Use: "block [username]", Short: "Block a user", - Example: " useradm block bpt22", + Example: " useradm user block bpt22", Args: cobra.ExactArgs(1), - RunE: BlockUserCmd, + RunE: blockUserCmd, } -func BlockUserCmd(cmd *cobra.Command, args []string) error { +func init() { + BlockCmd.Flags().BoolP("confirm", "y", false, "Skip confirmation prompt") +} + +func blockUserCmd(cmd *cobra.Command, args []string) error { + confirm, err := cmd.Flags().GetBool("confirm") + if err != nil { + return err + } + l, err := model.ConnLDAP() if err != nil { return err } defer l.Close() - login := args[0] - u, err := model.Locate(l, login) + // Locate returns a error if more than one user was found + u, err := model.Locate(l, args[0]) if err != nil { return err } fmt.Printf("%v\n", u.FullToString()) + utils.ConfirmationPrompt(confirm, "user blocking") + + err = u.Block() + if err != nil { + return err + } - utils.ConfirmationPrompt(false, "blocking") - u.Block() + fmt.Println("User blocked!") return nil } diff --git a/cmd/user/create.go b/cmd/user/create.go index 1c430db..73ac9f3 100644 --- a/cmd/user/create.go +++ b/cmd/user/create.go @@ -3,7 +3,6 @@ package user import ( "fmt" - "github.com/go-ldap/ldap/v3" "github.com/spf13/cobra" "gitlab.c3sl.ufpr.br/tss24/useradm/model" "gitlab.c3sl.ufpr.br/tss24/useradm/utils" @@ -48,7 +47,7 @@ func createUserCmd(cmd *cobra.Command, args []string) error { defer l.Close() // creates model from users input - usr, confirm, err := createNewUserModel(cmd, l) + usr, confirm, err := createNewUserModel(cmd) if err != nil { return err } @@ -61,12 +60,14 @@ func createUserCmd(cmd *cobra.Command, args []string) error { fmt.Printf("%v\n", usr.FullToString()) // for debug //fmt.Printf("%v\n Passwd: %v\n\n", usr.ToString(), usr.Password) + // print a warning if name was found, + // better because UID is forced to be unique if model.NameExists(users, usr.Name) { fmt.Printf("WARNING: Name was found in LDAP database!\n") confirm = false } - utils.ConfirmationPrompt(confirm, "creation") + utils.ConfirmationPrompt(confirm, "user creation") err = usr.Create(l) if err != nil { @@ -82,7 +83,7 @@ func createUserCmd(cmd *cobra.Command, args []string) error { } // creates and validates user inputs into the User model -func createNewUserModel(cmd *cobra.Command, l *ldap.Conn) (model.User, bool, error) { +func createNewUserModel(cmd *cobra.Command) (model.User, bool, error) { var u model.User var opts model.Opts @@ -119,6 +120,7 @@ func createNewUserModel(cmd *cobra.Command, l *ldap.Conn) (model.User, bool, err u.Ltype.Parse(opts.Ltype) + // this will infer the fields the user didn't provide (blank) err = u.GenMissingFields() if err != nil { return u, false, err diff --git a/cmd/user/group.go b/cmd/user/group.go new file mode 100644 index 0000000..6242aa8 --- /dev/null +++ b/cmd/user/group.go @@ -0,0 +1,62 @@ +package user + +import ( + "fmt" + + "github.com/spf13/cobra" + "gitlab.c3sl.ufpr.br/tss24/useradm/model" + "gitlab.c3sl.ufpr.br/tss24/useradm/utils" +) + +var GroupCmd = &cobra.Command{ + Use: "group [username] [new-group]", + Short: "Change a user's base group", + Example: " useradm user group temp2 prof", + Args: cobra.ExactArgs(2), + RunE: groupUserCmd, +} + +func init() { + GroupCmd.Flags().BoolP("confirm", "y", false, "Skip confirmation prompt") +} + +func groupUserCmd(cmd *cobra.Command, args []string) error { + confirm, err := cmd.Flags().GetBool("confirm") + if err != nil { + return err + } + + l, err := model.ConnLDAP() + if err != nil { + return err + } + defer l.Close() + + // Locate returns a error if more than one user was found + u, err := model.Locate(l, args[0]) + if err != nil { + return err + } + + groups, err := model.GetAllGroupsLDAP(l) + if err != nil { + return err + } + + _, err = utils.GetGIDNumFromGID(groups, args[1]) + if err != nil { + return err + } + + fmt.Printf("%v\n", u.FullToString()) + utils.ConfirmationPrompt(confirm, "user group change") + + err = u.ChangeBaseGroupLDAP(l, args[1]) + if err != nil { + return err + } + + fmt.Println("Base group updated!") + + return nil +} diff --git a/cmd/user/mod.go b/cmd/user/mod.go index 9b9b1d7..5a81ddf 100644 --- a/cmd/user/mod.go +++ b/cmd/user/mod.go @@ -108,7 +108,7 @@ func modUserCmd(cmd *cobra.Command, args []string) error { } fmt.Printf("%v\n\n", curr.ToString()) - utils.ConfirmationPrompt(opts.Confirm, "update") + utils.ConfirmationPrompt(opts.Confirm, "user update") if err := l.Modify(req); err != nil { return fmt.Errorf("Failed to update user attributes: %v", err) diff --git a/cmd/user/remove.go b/cmd/user/remove.go index 5f2debf..58a40e4 100644 --- a/cmd/user/remove.go +++ b/cmd/user/remove.go @@ -25,19 +25,18 @@ func removeUserCmd(cmd *cobra.Command, args []string) error { var opts model.Opts success := false - l, err := model.ConnLDAP() + err := opts.RetrieveOpts(cmd) if err != nil { return err } - defer l.Close() - err = opts.RetrieveOpts(cmd) + l, err := model.ConnLDAP() if err != nil { return err } + defer l.Close() - login := args[0] - u, err := model.Locate(l, login) + u, err := model.Locate(l, args[0]) if err != nil { return err } @@ -56,7 +55,7 @@ func removeUserCmd(cmd *cobra.Command, args []string) error { fmt.Printf("Found %v\n\n", u.FullToString()) - utils.ConfirmationPrompt(opts.Confirm, "removal") + utils.ConfirmationPrompt(opts.Confirm, "user removal") err = u.DirsToTrash() if err != nil { @@ -64,7 +63,7 @@ func removeUserCmd(cmd *cobra.Command, args []string) error { // 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, - // its better not to try to fix it. Just warn the admin + // 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 @@ -77,7 +76,7 @@ func removeUserCmd(cmd *cobra.Command, args []string) error { return err } - fmt.Printf("\nUser removed!\n") + fmt.Println("User removed!") success = true return nil diff --git a/cmd/user/show.go b/cmd/user/show.go index 6bb389e..35dd399 100644 --- a/cmd/user/show.go +++ b/cmd/user/show.go @@ -56,9 +56,9 @@ func searchUserCmd(cmd *cobra.Command, args []string) error { return nil } - for i := range found { + for _, u := range found { //fmt.Printf("%v\n\n", found[i].ToString()) - fmt.Printf("%v\n\n", found[i].FullToString()) // for debugging + fmt.Printf("%v\n\n", u.FullToString()) // for debugging } return nil diff --git a/cmd/user/temp.go b/cmd/user/temp.go index ff46b6d..0af802a 100644 --- a/cmd/user/temp.go +++ b/cmd/user/temp.go @@ -1,6 +1,5 @@ package user -// TODO: PQ Q N FUNCIONA?????????????? import ( "fmt" "os" @@ -171,7 +170,7 @@ func createTempUser(base model.User, num int) error { } fmt.Printf("Pronto para criar:\n%v\n\n", base.FullToString()) - utils.ConfirmationPrompt(false, "creation") + utils.ConfirmationPrompt(false, "user creation") err = base.Create(l) if err != nil { diff --git a/cmd/user/unblock.go b/cmd/user/unblock.go index 56ec9b6..701efd1 100644 --- a/cmd/user/unblock.go +++ b/cmd/user/unblock.go @@ -7,18 +7,22 @@ import ( ) var UnblockCmd = &cobra.Command{ - Use: "unblock [username]", - Short: "Unblock a user", - Args: cobra.ExactArgs(1), - RunE: UnblockUserCmd, + Use: "unblock [username]", + Short: "Unblock a user", + Example: " useradm user unblock bpt22", + Args: cobra.ExactArgs(1), + RunE: unblockUserCmd, } func init() { UnblockCmd.Flags().StringP("passwd", "p", "_", "User's new password") + UnblockCmd.Flags().BoolP("confirm", "y", false, "Skip confirmation prompt") } -func UnblockUserCmd(cmd *cobra.Command, args []string) error { - pass, err := cmd.Flags().GetString("passwd") +func unblockUserCmd(cmd *cobra.Command, args []string) error { + var opts model.Opts + + err := opts.RetrieveOpts(cmd) if err != nil { return err } @@ -35,7 +39,7 @@ func UnblockUserCmd(cmd *cobra.Command, args []string) error { return err } - utils.ConfirmationPrompt(false, "unblocking") - u.Unblock(pass) + utils.ConfirmationPrompt(opts.Confirm, "user unblocking") + u.Unblock(opts.Passwd) return nil } diff --git a/model/ldap.go b/model/ldap.go index 10e6f82..4f77762 100644 --- a/model/ldap.go +++ b/model/ldap.go @@ -119,6 +119,38 @@ func DelFromLDAP(UID string) error { return nil } +func LDAPCreateGroup(conn *ldap.Conn, GID, GIDNumber string) error { + // contruct dn + dn := fmt.Sprintf("cn=%s,ou=grupos,dc=c3local", GID) + + 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 := conn.Add(req); err != nil { + return fmt.Errorf("failed to add group: %w", err) + } + + return nil +} + +func LDAPDeleteGroup(conn *ldap.Conn, GID string) error { + // construct dn + dn := fmt.Sprintf("cn=%s,ou=grupos,dc=c3local", GID) + + delReq := ldap.NewDelRequest(dn, []ldap.Control{}) + + if err := conn.Del(delReq); err != nil { + return fmt.Errorf("failed to delete group: %w", err) + } + + return nil +} + func DelFromGroupLDAP(l *ldap.Conn, userUID, GID string) error { groupDN := fmt.Sprintf("cn=%s,ou=grupos,dc=c3local", GID) modReq := ldap.NewModifyRequest(groupDN, nil) @@ -146,6 +178,17 @@ func AddToGroupLDAP(l *ldap.Conn, UID, GID string) error { return nil } +func (u *User) ChangeBaseGroupLDAP(l *ldap.Conn, newGID string) error { + req := ldap.NewModifyRequest(u.DN, []ldap.Control{}) + req.Replace("gidNumber", []string{newGID}) + + if err := l.Modify(req); err != nil { + return fmt.Errorf("failed to modify user's base group: %w", err) + } + + return nil +} + func GetAllUsersLDAP(l *ldap.Conn) ([]User, error) { var users []User @@ -215,7 +258,7 @@ func GetAllGroupsLDAP(l *ldap.Conn) (map[string]string, error) { groupMap := make(map[string]string) // build search - searchRequest := ldap.NewSearchRequest( + req := ldap.NewSearchRequest( "dc=c3local", ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, @@ -226,7 +269,7 @@ func GetAllGroupsLDAP(l *ldap.Conn) (map[string]string, error) { ) // search - sr, err := l.Search(searchRequest) + sr, err := l.Search(req) if err != nil { return nil, fmt.Errorf("LDAP search failed: %w", err) } @@ -247,7 +290,7 @@ func GetAllUIDsLDAP(l *ldap.Conn) ([]int, error) { var uidNumbers []int // build search - searchRequest := ldap.NewSearchRequest( + req := ldap.NewSearchRequest( "dc=c3local", ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, @@ -258,7 +301,7 @@ func GetAllUIDsLDAP(l *ldap.Conn) ([]int, error) { ) // search - sr, err := l.Search(searchRequest) + sr, err := l.Search(req) if err != nil { return nil, fmt.Errorf("LDAP search failed: %w", err) } @@ -280,3 +323,41 @@ func GetAllUIDsLDAP(l *ldap.Conn) ([]int, error) { return uidNumbers, nil } + +func GetAllGIDsLDAP(l *ldap.Conn) ([]int, error) { + var gidNumbers []int + + // build search + req := ldap.NewSearchRequest( + "dc=c3local", + ldap.ScopeWholeSubtree, + ldap.NeverDerefAliases, + 0, 0, false, + "(&(objectClass=posixGroup))", + []string{"gidNumber"}, + nil, + ) + + // search + sr, err := l.Search(req) + if err != nil { + return nil, fmt.Errorf("LDAP search failed: %w", err) + } + + // arrange result into a array of integers + for _, entry := range sr.Entries { + gidStr := entry.GetAttributeValue("gidNumber") + if gidStr != "" { + gid, err := strconv.Atoi(gidStr) + if err != nil { + return nil, fmt.Errorf("invalid GID number: %s", gidStr) + } + gidNumbers = append(gidNumbers, gid) + } + } + + // sort for ease of use :) + sort.Ints(gidNumbers) + + return gidNumbers, nil +} diff --git a/model/user.go b/model/user.go index 0a04d47..a1f4510 100644 --- a/model/user.go +++ b/model/user.go @@ -16,6 +16,8 @@ import ( const ( MIN_UID = 1000 // up to 1000 are system users, 0 is root MAX_UID = 30000 // max UIDNumber permitted + MIN_GID = 1000 // up to 1000 are system users, 0 is root + MAX_GID = 5000 // max GIDNumber permitted MAX_VARIANCE = 60 // stops iteration at some point DEF_PERMISSION = "0700" // default user dir permission BLK_PERMISSION = "0000" // user dir permission if blocked @@ -237,6 +239,28 @@ func (u *User) GetNewUIDNumber(l *ldap.Conn) error { return nil } +func GetNewGIDNumber(l *ldap.Conn) (string, error) { + uids, err := GetAllGIDsLDAP(l) + if err != nil { + return "", err + } + + candidate := MIN_GID + for _, uid := range uids { + if uid == candidate { // check if taken + candidate++ + } else if uid > candidate { // found a gap + break + } + } + + if candidate > MAX_GID { + return "", fmt.Errorf("No more available GID numbers") + } + + return strconv.Itoa(candidate), nil +} + // creates the login, if it already exists try again with variance func (u *User) GenUniqueUID(users []User) error { used, variance := true, 0 diff --git a/utils/utils.go b/utils/utils.go index 4630f35..a916532 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -143,7 +143,7 @@ func MoveAndChown(orig, dest, owner, group string) error { // prints a confirmation prompt, given the operation being performed func ConfirmationPrompt(confirm bool, operation string) { if !confirm { - fmt.Printf("Proceed with user %v? [y/N] ", operation) + fmt.Printf("Proceed with %v? [y/N] ", operation) reader := bufio.NewReader(os.Stdin) response, _ := reader.ReadString('\n') -- GitLab