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