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