From 080d7ef182f2928e27127710028852fa55eeb170 Mon Sep 17 00:00:00 2001 From: tss24 <tss24@inf.ufpr.br> Date: Thu, 27 Feb 2025 14:43:05 -0300 Subject: [PATCH] Fix group attributions --- README.md | 36 +++++++++++++++++++----------------- cmd/user/create.go | 20 ++++++++++++++++++++ cmd/user/mod.go | 29 +++++++++++++++++++++++++++-- cmd/user/remove.go | 38 ++++++++++++++++++++++++-------------- cmd/user/reset.go | 2 +- cmd/user/show.go | 40 ++++++++++++++++++++-------------------- cmd/user/validation.go | 2 +- model/opts.go | 9 ++++++++- 8 files changed, 120 insertions(+), 56 deletions(-) diff --git a/README.md b/README.md index 2462509..e24a67a 100644 --- a/README.md +++ b/README.md @@ -12,17 +12,16 @@ and build the binary: cd useradm && go build -o useradm -if you want add it the go path use: +if you want, add it the go path use: go install - ## Usage For each command given we want to know what we will interact with (user/group), -as well as the action (create/remove/etc). The command will be asking for -confirmation in commands where some change is made so you can preview and deny -if something is not as you wanted. +as well as the action (create/remove/etc). The CLI will ask for confirmation +in commands where some change is made so you can preview and denyif something +is not as you wanted. Note 1: order of flags does not matter @@ -30,20 +29,20 @@ Note 2: most commands have the -y flag to skip the confirmation if you want ### Show -To show a user's info we can use: +To show a user's info you can use: useradm user show [options] -in flags the command needs identifiers to make the search, such as: +the command needs identifiers to make the search, such as: useradm user show -n pedro -r 2024 -g bcc -i -here we are searching for a user with name with pedro, grr with 2024, is part -of the group 'bcc' and -i is to make the search case insensitive. +here it is searching for a user with name with pedro, grr with 2024, is part of +the group 'bcc' and -i is to make the search case insensitive. ### Create -To create a user we can use: +To create a user you can use: useradm user create [options] @@ -64,7 +63,7 @@ the auto generation will be explained in the section "Implementation" ### Remove -To remove a user, since the login name is guaranteed to be unique, we do: +To remove a user, since the login name is guaranteed to be unique, you can use: useradm user remove [username] @@ -75,14 +74,16 @@ be removed from LDAP and Kerberos, their directories will end up at: webdir: /home/contas_removidas/html/<Year of removal>/ webdir: /nobackup/contas_removidas/<Year of removal>/ +You can modify these paths in `/cmd/user/create.go` + ### Modify -To modify a user we can do: +To modify a user you can do: useradm user mod [username] after pressing return a temp file with the user config will be opened in an -editor (based on your terminal varible $EDITOR). +editor (based on your terminal varible `$EDITOR`). After editing you can save and exit the file and the modifications will be read, shown for confirmation and applied. @@ -112,7 +113,6 @@ TODO :<<<<< TODO :&&&&&&& - ## Implementation To make this binary I used Go's cobra CLI module since it is common and I wanted @@ -154,13 +154,13 @@ not accepted so the function for it is a little different (PS: don't judge me) ### Validation The old command `useradm.py` didn't really validate the entries given and had no -confirmation. Because of that and some intelligence misfortunes there were/are +confirmation. Because of that ~and some intelligence misfortunes~ there were/are some users with messed up names, gecos, etc. So I made an effort on making a CLI that minimizes those misfortunes. ### Login generation -`useradm.py` had 3 function to generate names that used a very wierd algorithm. +`useradm.py` had 3 functions to generate names that used a very wierd algorithm. At first I just copied them, then I thought it was too long and repetitive so I merged them into one (See commit e49eca75). It was VERY ugly, so Fernando K. helped and rewrote/improved the algorithm to a more mantainable state, thanks :) @@ -200,9 +200,11 @@ and so on. 'last' would generate: and so on... +You may use `/cmd/user/create_test.go` if you want a test for the algorithm. + Essentially we add one letter to each name part for each variance added. We also remove any connectives (do, da, de, dos, von...) from the name. If there comes a -time a person appears with a new connective, update the formatName() function +time a person appears with a new connective, update the `formatName()` function currently at `/cmd/user/create.go` to account for that :) diff --git a/cmd/user/create.go b/cmd/user/create.go index cb79963..a7bd49e 100644 --- a/cmd/user/create.go +++ b/cmd/user/create.go @@ -50,6 +50,7 @@ func init() { // required flags CreateUserCmd.MarkFlagRequired("name") CreateUserCmd.MarkFlagRequired("group") + // made it required, may be overkill... CreateUserCmd.MarkFlagRequired("course") CreateUserCmd.Flags().BoolP("confirm", "y", false, "Skip confirmation prompt") @@ -380,6 +381,25 @@ func addUserLDAP(u model.User) error { return fmt.Errorf("Failed to add user %s to LDAP: %v", u.UID, err) } + err = addUserGroupLDAP(l, u.UID, u.GID) + if err != nil { + return err + } + + return nil +} + +// adds a user to a group in LDAP +func addUserGroupLDAP(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}) + + err := l.Modify(modReq) + if err != nil { + return fmt.Errorf("Failed to add user %s to group %s: %v", UID, GID, err) + } + return nil } diff --git a/cmd/user/mod.go b/cmd/user/mod.go index 4cfc382..6792f92 100644 --- a/cmd/user/mod.go +++ b/cmd/user/mod.go @@ -1,6 +1,5 @@ package user -// TODO: fix group assignments import ( "bufio" "fmt" @@ -33,15 +32,25 @@ var ModCmd = &cobra.Command{ RunE: modifyUserFunc, } +func init() { + ModCmd.Flags().BoolP("confirm", "y", false, "Skip confirmation prompt") +} + func modifyUserFunc(cmd *cobra.Command, args []string) error { var users []model.User + var opts model.Opts users, err := getUsers() if err != nil { return err } + err = opts.RetrieveOpts(cmd) + if err != nil { + return err + } + login := args[0] - res := searchUser(users, false, login, "", "", "", "", "") + res := searchUser(users, false, true, login, "", "", "", "", "") if len(res) != 1 { err = fmt.Errorf("More than one user found") return err @@ -83,11 +92,16 @@ func modifyUserFunc(cmd *cobra.Command, args []string) error { return err } + oldGroup := curr.GID + req, err := genRequest(curr, changes) if err != nil { return err } + fmt.Printf("%v\n\n", curr.ToString()) + confirmationPrompt(opts.Confirm, "update") + l, err := connLDAP() if err != nil { return err @@ -98,6 +112,16 @@ func modifyUserFunc(cmd *cobra.Command, args []string) error { return fmt.Errorf("Failed to update user attributes: %v", err) } + if oldGroup != changes.Group { + if err := delUserFromGroupLDAP(l, curr.UID, curr.GID); err != nil { + return err + } + + if err := addUserGroupLDAP(l, curr.UID, changes.Group); err != nil { + return err + } + } + if err := clearCache(); err != nil { fmt.Printf(`Failed to reload cache! all is ok but may take a while to apply the changes @@ -165,6 +189,7 @@ func genRequest(curr model.User, changes cfg) (*ldap.ModifyRequest, error) { func applyChangesToUser(c model.User, n cfg) model.User { c.GRR = n.GRR + c.GID = n.Group c.Name = n.Name c.Resp = n.Resp c.Shell = n.Shell diff --git a/cmd/user/remove.go b/cmd/user/remove.go index 0796886..499c5b3 100644 --- a/cmd/user/remove.go +++ b/cmd/user/remove.go @@ -24,7 +24,7 @@ var ( var RemoveUserCmd = &cobra.Command{ Use: "remove [username]", Short: "Delete a user", - Args: cobra.ExactArgs(1), + Args: cobra.ExactArgs(1), RunE: removeUserFunc, } @@ -41,7 +41,7 @@ func removeUserFunc(cmd *cobra.Command, args []string) error { return err } - login := args[0] + login := args[0] u, err := locateUser(login) if err != nil { return err @@ -88,7 +88,7 @@ func locateUser(login string) (model.User, error) { return u, fmt.Errorf("Failed to find login in LDAP database: %v", err) } - filter := searchUser(users, false, login, "", "", "", "", "") + filter := searchUser(users, false, true, 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) @@ -125,29 +125,26 @@ func delUserLDAP(u model.User) error { } defer l.Close() - // first remove user from all groups + // search for all groups the user is a member of 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"}, + "(memberUid="+u.UID+")", // Filter by UID membership + []string{"dn", "cn"}, nil, ) - // searches groups for target groups, err := l.Search(searchReq) if err != nil && !ldap.IsErrorWithCode(err, ldap.LDAPResultNoSuchObject) { return fmt.Errorf("Group members search failed: %v", err) } - // removing from groups + // iterate and remove user from each group 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) + groupName := entry.GetAttributeValue("cn") + if err := delUserFromGroupLDAP(l, u.UID, groupName); err != nil { + log.Printf("Warning: %v", err) } } @@ -155,7 +152,20 @@ func delUserLDAP(u model.User) error { 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 fmt.Errorf("User deletion failed: %v", err) + } + + return nil +} + +func delUserFromGroupLDAP(l *ldap.Conn, userUID, GID string) error { + groupDN := fmt.Sprintf("cn=%s,ou=grupos,dc=c3local", GID) + 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 nil diff --git a/cmd/user/reset.go b/cmd/user/reset.go index 1df6ea5..038e918 100644 --- a/cmd/user/reset.go +++ b/cmd/user/reset.go @@ -31,7 +31,7 @@ func resetPass(cmd *cobra.Command, args []string) error { } login := args[0] - res := searchUser(users, false, login, "", "", "", "", "") + res := searchUser(users, false, true, login, "", "", "", "", "") if len(res) != 1 { return fmt.Errorf("More than one user found") } diff --git a/cmd/user/show.go b/cmd/user/show.go index 1bbbc57..6de2eee 100644 --- a/cmd/user/show.go +++ b/cmd/user/show.go @@ -36,6 +36,7 @@ func init() { ShowCmd.MarkFlagsOneRequired("grr", "name", "login", "group", "homedir", "status") ShowCmd.Flags().BoolP("ignore", "i", false, "Make the search case-insensitive") + ShowCmd.Flags().BoolP("exact", "e", false, "Make the matching be exact") } func searchUserFunc(cmd *cobra.Command, args []string) error { @@ -51,13 +52,13 @@ func searchUserFunc(cmd *cobra.Command, args []string) error { return err } - filtered := searchUser(users, o.Ignore, o.UID, o.GID, + filtered := searchUser(users, o.Ignore, o.Exact, o.UID, o.GID, o.Name, o.GRR, o.Status, o.Homedir) - if len(filtered) == 0 { - fmt.Printf("No user matched the search!") - return nil - } + if len(filtered) == 0 { + fmt.Printf("No user matched the search!") + return nil + } for i := range filtered { fmt.Printf("%v\n\n", filtered[i].ToString()) @@ -66,26 +67,25 @@ func searchUserFunc(cmd *cobra.Command, args []string) error { return nil } -func searchUser(users []model.User, ig bool, l, g, n, r, s, h string) []model.User { - +func searchUser(users []model.User, ig, ex bool, l, g, n, r, s, h string) []model.User { return Filter(users, func(u model.User) bool { - contains := func(src, substr string) bool { + matches := func(src, target string) bool { if ig { - return strings.Contains( // normalize if set to ignore - strings.ToLower(src), - strings.ToLower(substr), - ) + src, target = strings.ToLower(src), strings.ToLower(target) + } + if ex { + return src == target // exact match } - return strings.Contains(src, substr) // normal search + return strings.Contains(src, target) // partial match } - // Search - return (r == "" || contains(u.GRR, r)) && - (n == "" || contains(u.Name, n)) && - (l == "" || contains(u.UID, l)) && - (g == "" || contains(u.GID, g)) && - (s == "" || contains(u.Status, s)) && - (h == "" || contains(u.Homedir, h)) + // search + return (r == "" || matches(u.GRR, r)) && + (n == "" || matches(u.Name, n)) && + (l == "" || matches(u.UID, l)) && + (g == "" || matches(u.GID, g)) && + (s == "" || matches(u.Status, s)) && + (h == "" || matches(u.Homedir, h)) }) } diff --git a/cmd/user/validation.go b/cmd/user/validation.go index 474cf8e..26aaeaa 100644 --- a/cmd/user/validation.go +++ b/cmd/user/validation.go @@ -94,7 +94,7 @@ func validateUID(login string) error { if err != nil { return err } - res := searchUser(users, false, login, "", "", "", "", "") + res := searchUser(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) diff --git a/model/opts.go b/model/opts.go index 6f2829f..93d947a 100644 --- a/model/opts.go +++ b/model/opts.go @@ -22,6 +22,7 @@ type Opts struct { Expiry string Homedir string Ignore bool + Exact bool Block bool Unblock bool Confirm bool @@ -44,12 +45,13 @@ func (o *Opts) ToString() string { Expiry %s Homedir %s Ignore %v + Exact %v Block %v Unblock %v Confirm %v`, o.GRR, o.GID, o.UID, o.Resp, o.Name, o.Nobkp, o.Ltype, o.Shell, o.Passwd, o.Webdir, o.Status, o.Course, o.Expiry, o.Homedir, o.Ignore, - o.Block, o.Unblock, o.Confirm) + o.Exact, o.Block, o.Unblock, o.Confirm) } func (o *Opts) RetrieveOpts(cmd *cobra.Command) error { @@ -145,6 +147,11 @@ func (o *Opts) RetrieveOpts(cmd *cobra.Command) error { return err } + o.Exact, err = getFlagBool(cmd, "exact") + if err != nil { + return err + } + return nil } -- GitLab