From 33987e52ad07ca144a158aaedebb84dfae5b5d86 Mon Sep 17 00:00:00 2001
From: tss24 <tss24@inf.ufpr.br>
Date: Thu, 27 Mar 2025 11:24:00 -0300
Subject: [PATCH] Refactor bulk subcommand

---
 README.md            | 227 ++++++++++++++++++++++++++------------
 cmd/bulk.go          |  99 ++++++++++-------
 cmd/user.go          |   1 -
 cmd/user/bulk.go     | 102 ------------------
 cmd/user/create.go   |  17 ++-
 cmd/user/remove.go   |   9 ++
 model/user.go        | 251 ++++++++++++++++++++++---------------------
 utils/utils.go       |   6 ++
 validate/validate.go |   3 +-
 9 files changed, 382 insertions(+), 333 deletions(-)
 delete mode 100644 cmd/user/bulk.go

diff --git a/README.md b/README.md
index a44b3a8..42e9512 100644
--- a/README.md
+++ b/README.md
@@ -6,36 +6,48 @@ This command is a CLI for managing users and groups in a LDAP/KRB environment.
 
 To install, just clone the repo:
 
-    git clone https://gitlab.c3sl.ufpr.br/tss24/useradm.git
+```sh
+git clone https://gitlab.c3sl.ufpr.br/tss24/useradm.git
+```
 
 and build the binary:
 
-    cd useradm && go build -o useradm
+```sh
+cd useradm && go build -o useradm
+```
 
 if you want, add it the go path use:
 
-    go install
+```sh
+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 CLI will ask for confirmation 
-in commands where some change is made so you can preview and denyif something 
+as well as the action (create/remove/...). The CLI will ask for confirmation 
+in commands where some change is made, so you can preview and deny if something 
 is not as you wanted.
 
-Note 1: order of flags does not matter
+>Note 1: order of flags does not matter
 
-Note 2: most commands have the -y flag to skip the confirmation if you want
+>Note 2: most commands have the -y flag to skip the confirmation if you want
+
+## User
 
 ### Show
 
 To show a user's info you can use:
 
-    useradm user show [options]
+```sh
+useradm user show [options]
+```
 
 the command needs identifiers to make the search, such as:
 
-    useradm user show -n pedro -r 2024 -g bcc -i 
+```sh
+useradm user show -n pedro -r 2024 -g bcc -i 
+```
 
 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.
@@ -44,20 +56,28 @@ the group 'bcc' and -i is to make the search case insensitive.
 
 To create a user you can use:
 
-    useradm user create [options]
+```sh
+useradm user create [options]
+```
 
 the command needs some basic info to create a user. The baseline is this:
 
-    useradm user create -n "<User Full Name>" -g <User group>
+```sh
+useradm user create -n "<User Full Name>" -g <User group>
+```
 
 but if we run something like 
 
-    useradm user create -n "Joao da Silva" -g bcc
+```sh
+useradm user create -n "Joao da Silva" -g bcc
+```
 
 we will get an error. That is because the login generation needs the user GRR 
 for the default 'ini' login type. So this is now correct:
 
-    useradm user create -n "Joao da Silva" -g bcc -t last
+```sh
+useradm user create -n "Joao da Silva" -g bcc -t last
+```
 
 the auto generation will be explained in the section "Implementation"
 
@@ -65,14 +85,18 @@ the auto generation will be explained in the section "Implementation"
 
 To remove a user, since the login name is guaranteed to be unique, you can use:
 
-    useradm user remove [username]
+```sh
+useradm user remove [username]
+```
 
 then the users info will be displayed for confirmation and their account will
 be removed from LDAP and Kerberos, their directories will end up at:
 
-    home: /home/contas_removidas/<Year of removal>/
-    webdir: /home/contas_removidas/html/<Year of removal>/
-    webdir: /nobackup/contas_removidas/<Year of removal>/
+```sh
+home: /home/contas_removidas/[Year of removal]/
+webdir: /home/contas_removidas/html/[Year of removal]/
+nobackup: /nobackup/contas_removidas/[Year of removal]/
+```
 
 You can modify these paths in `/cmd/user/create.go`
 
@@ -80,28 +104,54 @@ You can modify these paths in `/cmd/user/create.go`
 
 To modify a user you can do:
 
-    useradm user mod [username]
+```sh
+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`).
+after pressing return, a temp file with the user config will be opened in an
+editor (based on your terminal variable `$EDITOR`).
 
 After editing you can save and exit the file and the modifications will be read,
 shown for confirmation and applied.
 
+From the time of writting this it is kinda useless... That's because the easily
+changeable fiels are not really impactful so we don't change them. And the not 
+so easy ones are done so rarely that it is better to just do it in the shell 
+anyways.
+
 ### Reset
 
 The most often used modification gets it's own command.
 
 To reset a user's password you can do:
 
-    useradm user reset [flag]
+```sh
+useradm user reset [flag]
+```
 
 with no flag a password will be auto-generated. Passing the flag -p you can
-specify the new password. Passwords need to be good lol.
+specify the new password. Passwords need to be good, need 8 chars and 3 char 
+classes, such as number, special, upper/lowercase.
+
+### Block/Unblock
+
+To block a user you can do:
 
-### Bulk
+```sh
+useradm user block [username]
+```
 
-TODO :))))))
+this changes the loginShell to be `/bin/false`, changes the permission of their
+homedir/nobackup to 0000, resets their password, overkill(?).
+
+To unblock you can do:
+
+```sh
+useradm user unblock [username]
+```
+
+this restores all the permissions and sets a new password. You may set the new 
+password yourself using the `-p` flag.
 
 ### Migrate
 
@@ -111,50 +161,87 @@ TODO :<<<<<
 
 TODO :&&&&&&&
 
+## Bulk
+
+This command is a little different. When implementing I came across a shell 
+script: `cria-contas.sh`, that wraps the old `useradm.py` to create the accounts 
+and is/was the main way accounts were created by Bete.
+
+The purpose of this subcommand is to simulate the experience, though the script
+could create accounts for most of the groups, the command didn't follow a 
+consistent state, sometimes creating one account just from the command, 
+sometimes creating from a file, etc...
+
+My version only creates from a file, you specify the group and a file (kind of
+like a csv but not comma separated, : separated). The command will generate the
+missing config and create the users. Of course, for each user there will be a
+confirmation prompt that you can skip with `-y` flag.
+
+Some examples of usage are:
+
+```sh
+useradm bulk bcc ./calouros.txt
+useradm bulk ppginf ./matriculados-ppginf > saida.txt
+useradm bulk est ~/alunos-carlos -r "Carlos da FĂ­sica"
+useradm bulk temp ~/adm/contas-temp -e 26.04.20 -y
+```
+
+If you want to know how the file is read for each group use:
+
+```sh
+useradm bulk --help
+```
+
+It's explained there.
+
 ## Implementation
 
-To make this binary I used Go's cobra CLI module since it is common and I wanted
-to learn Go lol. It makes it easy to show help and parsing args/flags :D
+To make this binary I used spf13's cobra CLI module since it is common and I
+wanted to learn Go lol. It makes it easy to show help and parsing args/flags and
+I liked it :D
 
-This command is the evolution of useradm.py and was heavily based on it. It had
-no documentation besides the actual code with no comments. So for anyone that is
-going to change this in the future I will now explain how things work. I will 
-now explain some not so obvious parts.
+This command is the evolution of `useradm.py` and was heavily based on it. It 
+had no documentation besides the actual code with no comments. So for anyone 
+that is going to change this in the future I will now explain how things work. 
+Mainly the not so obvious parts.
 
 ### LDAP
 
 Since we use LDAP to store data about users I used the LDAP module to make and
 send requests. A good example is `user show` to use that command we first get
-all users and store them in a array of User models (structs). Then we filter
-based on the user input.
+all users and store them in a array of User structs. Then we filter based on 
+the user input.
 
 To connect to LDAP we need to connect to the socket, and bind it using some
 credentials. From the time of writting this (26/02/2025) there is a password
-file that is used for that in urquell lol. 
+file that is used for that in urquell lol. The file that deals with ldap is 
+in `./model/ldap.go`
 
 Then we generate the request that will give us all the users, get the response,
-and store the result in structs.
+and store the result in structs. For `user show`, there is a Filter function
+that, well, filters all the users based on the search.
 
 Many other requests are made in the code and you can learn the syntax by reading
-it or via the docs of the module. One thing I want to add it that in LDAP, group
+it or via the docs of the module. One thing I want to add is that in LDAP, group
 assingments need to be done by setting the user object to be member of the group
-and set the group object to have the user as a member. Interesting...
+and set the group object to have the user as a member.
 
 ### Kerberos
 
-You may have realized that we do not store user passords in LDAP. We store them
+You may have realized that we do not store user passords in LDAP. We manage them
 in Kerberos. After you created the LDAP entry you can create a KerberosPrincipal
-for the user that will take care of authentication and tokens. When you create
+for the user, that will take care of authentication and tokens. When you create
 a principal you need to set a password via the `kadmin.local` command, which is 
 cursed :p the command even returns 0 (success) even when the password change was 
-not accepted so the function for it is a little different (PS: don't judge me)
+not accepted so the function for it is a little different. Check kerberos the 
+functions in `./model/krb.go`
 
 ### 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
-some users with messed up names, gecos, etc. So I made an effort on making a CLI
-that minimizes those misfortunes.
+The old command `useradm.py` didn't really validate the entries given and had 
+no good confirmation of succes or fail. 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
 
@@ -170,40 +257,46 @@ user with name Fabiano Antunes Pereira de Souza, with GRR 20241982:
 
 'ini' would create the following (based on `variance` variable):
 
-    faps24
-    faaps24
-    faanps24
-    faanpes24
-    faanpeso24
-    fabanpeso24
+```sh
+faps24
+faaps24
+faanps24
+faanpes24
+faanpeso24
+fabanpeso24
+```
 
 and so on. 'first' would create something like this:
 
-    fabiano
-    fabianoa
-    fabianoap
-    fabianoaps
-    fabianoanps
-    fabianoanpes
-    fabianoanpeso
+```sh
+fabiano
+fabianoa
+fabianoap
+fabianoaps
+fabianoanps
+fabianoanpes
+fabianoanpeso
+```
 
 and so on. 'last' would generate:
 
-    fapsouza
-    faapsouza
-    faanpsouza
-    faanpesouza
-    fabanpesouza
-    fabantpesouza
+```sh
+fapsouza
+faapsouza
+faanpsouza
+faanpesouza
+fabanpesouza
+fabantpesouza
+```
 
 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 currently at `./utils/utils.go` to account for that :)
 
-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
-currently at `/cmd/user/create.go` to account for that :)
+You may use `./model/user_test.go` if you want a test for the algorithm.
 
 
 TODO: finish...
diff --git a/cmd/bulk.go b/cmd/bulk.go
index 24c26e1..3d75a75 100644
--- a/cmd/bulk.go
+++ b/cmd/bulk.go
@@ -9,12 +9,10 @@ import (
 	"github.com/spf13/cobra"
 	"gitlab.c3sl.ufpr.br/tss24/useradm/model"
 	"gitlab.c3sl.ufpr.br/tss24/useradm/utils"
-	//"gitlab.c3sl.ufpr.br/tss24/useradm/validate"
+	"gitlab.c3sl.ufpr.br/tss24/useradm/validate"
 )
 
 var (
-	// if you start asking yourself wtf is the difference between fis and fisica,
-	// im pretty sure it is that fisica has profs and/or MSc/Doctorate...
 	supp_groups = [...]string{"ppginf", "espinf", "temp",
 		"bcc", "ibm", "est", "fis"}
 )
@@ -24,11 +22,6 @@ var bulkCmd = &cobra.Command{
 	Args:  cobra.ExactArgs(2),
 	Short: "Subcommand to create a lot of accounts",
 	Long: `
-  This subcommand is used when you have a file with 
-  a lot of names and want to create the accounts. 
-  The next argument is the group of the users, so 
-  please take care when choosing it. 
-
   Be aware the results will be printed to the screen, 
   so if you want to save it, redirect the command's 
   output to a file!
@@ -38,7 +31,22 @@ var bulkCmd = &cobra.Command{
 
   If your group is not here and you want to create a
   lot of user similarly, contact root@c3sl.ufpr.br
-  for us to update the script.`,
+  for us to update the script.
+
+  Here is how the input file should be formatted
+  based on the group you want to create:
+
+  bcc, ibm, est, fis, temp:
+
+    name:grr
+    name:grr
+    name:grr
+
+  ppginf, espinf:
+
+    name
+    name
+    name`,
 	Example: `    
   useradm bulk bcc path/to/file                          # base command
   useradm bulk ibm path/to/file -e 12.03.25              # setting expiry  
@@ -50,6 +58,7 @@ var bulkCmd = &cobra.Command{
 func init() {
 	bulkCmd.Flags().StringP("resp", "r", "_", "Person responsible for the accounts")
 	bulkCmd.Flags().StringP("expiry", "e", "_", "Expiry date of the accounts, format dd.mm.yy")
+	bulkCmd.Flags().BoolP("confirm", "y", false, "Skip confirmation prompt")
 }
 
 func bulkCreate(cmd *cobra.Command, args []string) error {
@@ -88,51 +97,48 @@ func bulkCreate(cmd *cobra.Command, args []string) error {
 		return err
 	}
 
-	for _, u := range accounts {
+	for i, u := range accounts {
+
+		if utils.ContainsNumber(u.Name) {
+			fmt.Printf("Entry %v: name field contains a number\nSkipping...\n", i+1)
+			continue
+		}
 
-		err = u.GenMissingFields(false)
+		err := validate.GRR(users, u.GRR)
+		if err != nil {
+			fmt.Printf("Entry %v: malformed GRR: %v\nSkipping...\n", i+1, u.GRR)
+			continue
+		}
+
+		if u.Ltype == model.Initials && u.GRR == "_" {
+			fmt.Printf("Entry %v: ini login type needs GRR\n Skipping...", i+1)
+			continue
+		}
+
+		err = u.GenMissingFields()
 		if err != nil {
 			return err
 		}
 
-		if !model.LoginExists(users, u.UID) {
+		if !model.NameExists(users, u.Name) {
+			if !opts.Confirm {
+				fmt.Printf("Ready to create: %v\n", u.FullToString())
+			}
+			utils.ConfirmationPrompt(opts.Confirm, "creation")
 			err = u.Create(l)
 			if err != nil {
 				return err
 			}
 		} else {
-			fmt.Printf(`
-First login generation alternative for "%v" is already taken!
-User was NOT created!
-Please check if the user exists with:
-                
-  useradm user show -e -l %v
-  useradm user show -i -n "%v"
-
-If it really is a new user, use one of these: 
-
-  useradm user create -n "%v" -g %v -e %v -i %v -t %v -r %v -w     # with webdir
-  useradm user create -n "%v" -g %v -e %v -i %v -t %v -r %v        # no webdir
-`, u.UID, u.UID, u.Name, u.Name, u.GID, u.Expiry, u.Resp, u.Ltype.SmallString(),
-				u.GRR, u.Name, u.GID, u.Expiry, u.Resp, u.Ltype.SmallString(), u.GRR)
+			fmt.Printf("Provided name %v is already in LDAP database\n", u.Name)
+			fmt.Printf("Please confirm it is in fact a new user\n")
+			fmt.Printf("Example: useradm user show -n \"%v\" -i\n", u.Name)
 		}
-
 	}
 
 	return nil
 }
 
-func isGroupSupported(group string) error {
-	err := fmt.Errorf("Group %v is not supported, see help", group)
-	for _, g := range supp_groups {
-		if g == group {
-			err = nil
-			break
-		}
-	}
-	return err
-}
-
 func extractModelsFile(opts model.Opts, group, path string) ([]model.User, error) {
 	var users []model.User
 
@@ -155,7 +161,11 @@ func extractModelsFile(opts model.Opts, group, path string) ([]model.User, error
 		line := scanner.Text()
 		parts := strings.Split(line, ":")
 		u.Name = parts[0]
-		u.GRR = parts[1]
+		u.GRR = "_"
+		if group != "ppginf" && group != "espinf" {
+			u.GRR = parts[1]
+		}
+		u.UID = ""
 		u.GID = group
 		u.Ltype = ltype
 		u.Resp = opts.Resp
@@ -168,3 +178,14 @@ func extractModelsFile(opts model.Opts, group, path string) ([]model.User, error
 	}
 	return users, nil
 }
+
+func isGroupSupported(group string) error {
+	err := fmt.Errorf("Group %v is not supported, see help", group)
+	for _, g := range supp_groups {
+		if g == group {
+			err = nil
+			break
+		}
+	}
+	return err
+}
diff --git a/cmd/user.go b/cmd/user.go
index f43617b..91126a8 100644
--- a/cmd/user.go
+++ b/cmd/user.go
@@ -16,7 +16,6 @@ func init() {
 		user.ModCmd,
 		user.ShowCmd,
 		user.TempCmd,
-		//user.BulkCmd,
 		user.ResetCmd,
 		user.CreateCmd,
 		user.RemoveCmd,
diff --git a/cmd/user/bulk.go b/cmd/user/bulk.go
deleted file mode 100644
index 6c35444..0000000
--- a/cmd/user/bulk.go
+++ /dev/null
@@ -1,102 +0,0 @@
-package user
-
-import (
-	"bufio"
-	"fmt"
-	"os"
-	"strings"
-
-	"github.com/spf13/cobra"
-	"gitlab.c3sl.ufpr.br/tss24/useradm/model"
-	"gitlab.c3sl.ufpr.br/tss24/useradm/utils"
-)
-
-var BulkCmd = &cobra.Command{
-	Use:   "bulk",
-	Short: "Create a lot of users",
-	Long: `Takes in a file with user opts and creates the accounts on the given
-group. The file must be formatted like the following:
-
-grr:fullname
-grr:fullname
-grr:fullname`,
-	RunE: bulkCreate,
-}
-
-func init() {
-	BulkCmd.Flags().StringP("group", "g", "", "Base group of the accounts")
-	BulkCmd.Flags().StringP("infile", "i", "", "File path with all users")
-
-	BulkCmd.MarkFlagRequired("group")
-	BulkCmd.MarkFlagRequired("infile")
-}
-
-func bulkCreate(cmd *cobra.Command, args []string) error {
-	var opts model.Opts
-	var accounts []model.User
-
-	l, err := model.ConnLDAP()
-	if err != nil {
-		return err
-	}
-	defer l.Close()
-
-	err = opts.RetrieveOpts(cmd)
-	if err != nil {
-		return err
-	}
-
-	accounts, err = extractModelsFile(opts.Path, opts.GID)
-	if err != nil {
-		return err
-	}
-
-	for _, account := range accounts {
-
-		err = account.GenMissingFields(false)
-		if err != nil {
-			return err
-		}
-		//	if model.LoginExists(account.UID) {
-		//		return
-		//  }
-
-		err = account.Create(l)
-		if err != nil {
-			return err
-		}
-
-	}
-
-	return nil
-}
-
-func extractModelsFile(path, group string) ([]model.User, error) {
-	var users []model.User
-
-	if !utils.PathExists(path) {
-		return users, fmt.Errorf("Path: \"%v\": no such file", path)
-	}
-
-	file, err := os.Open(path)
-	if err != nil {
-		return users, fmt.Errorf("Error opening file %v", err)
-	}
-	defer file.Close()
-
-	scanner := bufio.NewScanner(file)
-	for scanner.Scan() {
-		var u model.User
-		line := scanner.Text()
-		parts := strings.Split(line, ":")
-		u.Name = parts[0]
-		u.GRR = parts[1]
-		u.GID = group
-		users = append(users, u)
-	}
-
-	if err := scanner.Err(); err != nil {
-		fmt.Println("Error reading file:", err)
-	}
-	return nil, nil
-}
diff --git a/cmd/user/create.go b/cmd/user/create.go
index 100d8ed..1c430db 100644
--- a/cmd/user/create.go
+++ b/cmd/user/create.go
@@ -53,9 +53,19 @@ func createUserCmd(cmd *cobra.Command, args []string) error {
 		return err
 	}
 
-	// prints info for confirmation
+	users, err := model.GetAllUsersLDAP(l)
+	if err != nil {
+		return err
+	}
+
 	fmt.Printf("%v\n", usr.FullToString()) // for debug
 	//fmt.Printf("%v\n	Passwd:  %v\n\n", usr.ToString(), usr.Password)
+
+	if model.NameExists(users, usr.Name) {
+		fmt.Printf("WARNING: Name was found in LDAP database!\n")
+		confirm = false
+	}
+
 	utils.ConfirmationPrompt(confirm, "creation")
 
 	err = usr.Create(l)
@@ -109,7 +119,10 @@ func createNewUserModel(cmd *cobra.Command, l *ldap.Conn) (model.User, bool, err
 
 	u.Ltype.Parse(opts.Ltype)
 
-	u.GenMissingFields(true)
+	err = u.GenMissingFields()
+	if err != nil {
+		return u, false, err
+	}
 
 	return u, opts.Confirm, nil
 }
diff --git a/cmd/user/remove.go b/cmd/user/remove.go
index 20246b6..5f2debf 100644
--- a/cmd/user/remove.go
+++ b/cmd/user/remove.go
@@ -60,6 +60,15 @@ func removeUserCmd(cmd *cobra.Command, args []string) error {
 
 	err = u.DirsToTrash()
 	if err != nil {
+		// scuffed i know, in the edge case there is already a
+		// 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
+		// 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
+		success = true
 		return err
 	}
 
diff --git a/model/user.go b/model/user.go
index 35c932f..0a04d47 100644
--- a/model/user.go
+++ b/model/user.go
@@ -14,9 +14,9 @@ import (
 )
 
 const (
-	MIN_UID          = 1000
-	MAX_UID          = 6000
-	MAX_VARIANCE     = 30
+	MIN_UID          = 1000   // up to 1000 are system users, 0 is root
+	MAX_UID          = 30000  // max UIDNumber permitted
+	MAX_VARIANCE     = 60     // stops iteration at some point
 	DEF_PERMISSION   = "0700" // default user dir permission
 	BLK_PERMISSION   = "0000" // user dir permission if blocked
 	HTML_BASE_PERM   = "0755" // set public_html to this on creation
@@ -24,11 +24,16 @@ const (
 )
 
 var (
-	ANO          = strconv.Itoa(time.Now().Year())
+	ANO = strconv.Itoa(time.Now().Year())
+
+	// where removed account dirs go
 	NO_BKP_TRASH = "/nobackup/contas_removidas/" + ANO
 	HOME_TRASH   = "/home/contas_removidas/" + ANO
 	WEB_TRASH    = "/home/contas_removidas/html/" + ANO
-	GID_HAS_WEB  = [...]string{"bcc", "ibm", "ppginf", "c3sl", "prof", "especial"}
+
+	// if group has web, used to infer data, if the group is not
+	// here, will set webdir to "_" meaning "don't generate"
+	GID_HAS_WEB = [...]string{"bcc", "ibm", "ppginf", "c3sl", "prof", "especial"}
 )
 
 type User struct {
@@ -51,41 +56,6 @@ type User struct {
 	UIDNumber string
 }
 
-// Gecos Fields:
-// 0. users full name
-// 1. users grr
-// 2. responsible professor
-// 3. account expiry date (may be used for deletion)
-// 4. account status (Blocked/Active)
-// 5. login type (ini, first or last)
-// 6. users webdir
-// 7. users nobackup dir
-// 8. to be continued...
-func (u *User) ParseGecos() {
-	result := [NUM_GECOS_FIELDS]string{0: "_"}
-	for i := range result {
-		result[i] = "_"
-	}
-	parts := strings.Split(u.Gecos, ",")
-
-	for i := 0; i < len(parts) && i < NUM_GECOS_FIELDS; i++ {
-		if strings.TrimSpace(parts[i]) != "" {
-			result[i] = parts[i]
-		}
-	}
-
-	u.GRR = result[1]
-	u.Resp = result[2]
-	u.Expiry = result[3]
-	// only set with gecos if empty
-	if u.Status == "_" {
-		u.Status = result[4]
-	}
-	u.Ltype.Parse(result[5])
-	u.Webdir = result[6]
-	u.Nobackup = result[7]
-}
-
 func (u *User) Create(l *ldap.Conn) error {
 	success := false
 
@@ -167,6 +137,77 @@ search made: "useradm user show -l %v -e"`, login)
 	return u, nil
 }
 
+func (u *User) Block() error {
+	err := KRBRandKey(u.UID) // new password, no one will know
+	if err != nil {
+		return err
+	}
+
+	cmd := exec.Command("chmod", "0000", u.Homedir)
+	if err := cmd.Run(); err != nil {
+		return fmt.Errorf("Failed to set permissions: %w", err)
+	}
+
+	cmd = exec.Command("chmod", "0000", u.Nobackup)
+	if err := cmd.Run(); err != nil {
+		return fmt.Errorf("Failed to set permissions: %w", err)
+	}
+
+	l, err := ConnLDAP()
+	if err != nil {
+		return err
+	}
+	defer l.Close()
+
+	mod := ldap.NewModifyRequest(u.DN, nil)
+	mod.Replace("loginShell", []string{"/bin/false"})
+
+	if err := l.Modify(mod); err != nil {
+		return fmt.Errorf("failed to update loginShell for %s: %w", u.UID, err)
+	}
+
+	u.Status = "Blocked"
+	return nil
+}
+
+func (u *User) Unblock(pass string) error {
+	cmd := exec.Command("chmod", "0700", u.Homedir)
+	if err := cmd.Run(); err != nil {
+		return fmt.Errorf("Failed to set permissions: %w", err)
+	}
+
+	cmd = exec.Command("chmod", "0700", u.Nobackup)
+	if err := cmd.Run(); err != nil {
+		return fmt.Errorf("Failed to set permissions: %w", err)
+	}
+
+	l, err := ConnLDAP()
+	if err != nil {
+		return err
+	}
+	defer l.Close()
+
+	mod := ldap.NewModifyRequest(u.DN, nil)
+	mod.Replace("loginShell", []string{"/bin/bash"})
+
+	if err := l.Modify(mod); err != nil {
+		return fmt.Errorf("Failed to update loginShell for %s: %w", u.UID, err)
+	}
+
+	if pass == "_" {
+		pass = utils.GenPassword()
+	}
+
+	err = ModKRBPassword(u.UID, pass)
+	if err != nil {
+		return err
+	}
+
+	u.Status = "Active"
+	fmt.Printf("New Password: %v\n", pass)
+	return nil
+}
+
 // DN formatting
 func (u *User) SetDN(UID string) {
 	u.DN = "uid=" + UID + ",ou=usuarios,dc=c3local"
@@ -212,7 +253,6 @@ func (u *User) GenUniqueUID(users []User) error {
 	return nil
 }
 
-// ye
 func (u *User) genLogin(variance int) string {
 	parts := utils.FormatName(u.Name)
 	if len(parts) == 0 || u.Ltype == LoginTypeUnknown {
@@ -279,6 +319,41 @@ func (u *User) GenGecos() {
 	u.Gecos += u.Nobackup
 }
 
+// Gecos Fields:
+// 0. users full name
+// 1. users grr
+// 2. responsible professor
+// 3. account expiry date (may be used for deletion)
+// 4. account status (Blocked/Active)
+// 5. login type (ini, first or last)
+// 6. users webdir
+// 7. users nobackup dir
+// 8. to be continued...
+func (u *User) ParseGecos() {
+	result := [NUM_GECOS_FIELDS]string{0: "_"}
+	for i := range result {
+		result[i] = "_"
+	}
+	parts := strings.Split(u.Gecos, ",")
+
+	for i := 0; i < len(parts) && i < NUM_GECOS_FIELDS; i++ {
+		if strings.TrimSpace(parts[i]) != "" {
+			result[i] = parts[i]
+		}
+	}
+
+	u.GRR = result[1]
+	u.Resp = result[2]
+	u.Expiry = result[3]
+	// only set with gecos if empty
+	if u.Status == "_" {
+		u.Status = result[4]
+	}
+	u.Ltype.Parse(result[5])
+	u.Webdir = result[6]
+	u.Nobackup = result[7]
+}
+
 func (u *User) ToString() string {
 	return fmt.Sprintf(`User:
 	Name:    %v
@@ -325,6 +400,12 @@ func LoginExists(users []User, login string) bool {
 	return utils.Exists(users, func(u User) bool { return u.UID == login })
 }
 
+func NameExists(users []User, name string) bool {
+	return utils.Exists(users, func(u User) bool {
+		return strings.ToLower(u.Name) == strings.ToLower(name)
+	})
+}
+
 // accepts GRRs in the format "GRRXXXXXXXX" aswell as just 8 digit number
 func GRRExists(users []User, grr string) bool {
 	return utils.Exists(users, func(u User) bool { return u.GRR == grr }) ||
@@ -479,7 +560,7 @@ func (u *User) CreateWeb() error {
 // needs the name, group, ltype.
 // will derivate the rest of the information.
 // if it can't infer the field, will set to "_"
-func (u *User) GenMissingFields(force bool) error {
+func (u *User) GenMissingFields() error {
 	var err error
 
 	l, err := ConnLDAP()
@@ -489,14 +570,14 @@ func (u *User) GenMissingFields(force bool) error {
 	defer l.Close()
 
 	if u.UID == "" {
-		if force {
-			users, err := GetAllUsersLDAP(l)
-			if err != nil {
-				return err
-			}
-			u.GenUniqueUID(users)
-		} else {
-			u.genLogin(0)
+		users, err := GetAllUsersLDAP(l)
+		if err != nil {
+			return err
+		}
+
+		err = u.GenUniqueUID(users)
+		if err != nil {
+			return err
 		}
 	}
 
@@ -561,73 +642,3 @@ func (u *User) GenMissingFields(force bool) error {
 
 	return nil
 }
-
-func (u *User) Block() error {
-	err := KRBRandKey(u.UID)
-	if err != nil {
-		return err
-	}
-	cmd := exec.Command("chmod", "0000", u.Homedir)
-	if err := cmd.Run(); err != nil {
-		return fmt.Errorf("Failed to set permissions: %w", err)
-	}
-
-	cmd = exec.Command("chmod", "0000", u.Nobackup)
-	if err := cmd.Run(); err != nil {
-		return fmt.Errorf("Failed to set permissions: %w", err)
-	}
-
-	l, err := ConnLDAP()
-	if err != nil {
-		return err
-	}
-	defer l.Close()
-
-	mod := ldap.NewModifyRequest(u.DN, nil)
-	mod.Replace("loginShell", []string{"/bin/false"})
-
-	if err := l.Modify(mod); err != nil {
-		return fmt.Errorf("failed to update loginShell for %s: %w", u.UID, err)
-	}
-
-	u.Status = "Blocked"
-	return nil
-}
-
-func (u *User) Unblock(pass string) error {
-	cmd := exec.Command("chmod", "0700", u.Homedir)
-	if err := cmd.Run(); err != nil {
-		return fmt.Errorf("Failed to set permissions: %w", err)
-	}
-
-	cmd = exec.Command("chmod", "0700", u.Nobackup)
-	if err := cmd.Run(); err != nil {
-		return fmt.Errorf("Failed to set permissions: %w", err)
-	}
-
-	l, err := ConnLDAP()
-	if err != nil {
-		return err
-	}
-	defer l.Close()
-
-	mod := ldap.NewModifyRequest(u.DN, nil)
-	mod.Replace("loginShell", []string{"/bin/bash"})
-
-	if err := l.Modify(mod); err != nil {
-		return fmt.Errorf("Failed to update loginShell for %s: %w", u.UID, err)
-	}
-
-	if pass == "_" {
-		pass = utils.GenPassword()
-	}
-
-	err = ModKRBPassword(u.UID, pass)
-	if err != nil {
-		return err
-	}
-
-	u.Status = "Active"
-	fmt.Printf("New Password: %v\n", pass)
-	return nil
-}
diff --git a/utils/utils.go b/utils/utils.go
index cc0fa14..4630f35 100644
--- a/utils/utils.go
+++ b/utils/utils.go
@@ -7,6 +7,7 @@ import (
 	"os"
 	"os/exec"
 	"path/filepath"
+	"regexp"
 	"strconv"
 	"strings"
 	"time"
@@ -85,6 +86,11 @@ func PathExists(path string) bool {
 	return false
 }
 
+func ContainsNumber(s string) bool {
+	regex := regexp.MustCompile(`\d`)
+	return regex.MatchString(s)
+}
+
 // do NOT include ':' in the charset, it WILL break the command
 func GenPassword() string {
 	const charset = "@*()=+[];,.?123456789abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ"
diff --git a/validate/validate.go b/validate/validate.go
index 67f2527..1ab8aa3 100644
--- a/validate/validate.go
+++ b/validate/validate.go
@@ -25,8 +25,7 @@ func Expiry(expiry string) error {
 // TODO: change this check
 func Status(status string) error {
 	if status != "Blocked" && status != "Active" {
-		err := fmt.Errorf("User status can only be \"Active\" or \"Blocked\"")
-		return err
+		return fmt.Errorf("User status can only be \"Active\" or \"Blocked\"")
 	}
 	return nil
 }
-- 
GitLab