diff --git a/cmd/root.go b/cmd/root.go index 76934e8282f0b48e11df356bc387e4cbb96ca0ed..c8d2f2d9bfc05357ab9eb3b9dd7b28c9bf46519d 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -14,9 +14,9 @@ var rootCmd = &cobra.Command{ with the cobra library that took inspiration from the old useradm.py. It takes care of LDAP configuration and was made as an update for the previous version.`, - // since we already print the errors in Execute() - // not having this would print the error twice :p - SilenceErrors: true, + // since we already print the errors in Execute() + // not having this would print the error twice :p + SilenceErrors: true, } func Execute() { diff --git a/cmd/user.go b/cmd/user.go index 8358eb268a7da069f032e98ddb593a65590c052e..2a4dafd010904b779e5d603cd3ef51c5e0ce8acc 100644 --- a/cmd/user.go +++ b/cmd/user.go @@ -8,7 +8,7 @@ import ( var userCmd = &cobra.Command{ Use: "user", Short: "User subcommand", - Long: `Subcommand for managing unique users in general. + Long: `Subcommand for managing unique users in general. If what you are trying to do is create a lot of new users, please do so with the bulk subcommand.`, } diff --git a/cmd/user/create.go b/cmd/user/create.go index 429e1c363e438b9717bd1ddfed88ab0ebe1bdb2c..26b41cb63f0cbd48ecbbf25ff6b0ff26f17cefed 100644 --- a/cmd/user/create.go +++ b/cmd/user/create.go @@ -1,28 +1,28 @@ package user import ( - "os" "fmt" - "time" - "regexp" + "os" "os/exec" + "path/filepath" + "regexp" "strconv" "strings" - "path/filepath" + "time" - "github.com/spf13/cobra" "github.com/go-ldap/ldap/v3" + "github.com/spf13/cobra" "gitlab.c3sl.ufpr.br/tss24/useradm/model" ) const ( - MAIL_ALIAS_FILE = "/etc/aliases" // aliases db path for query - 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 - MAX_VARIANCE = 50 // tries made to create login automatically - MIN_UID = 1000 - MAX_UID = 6000 + MAIL_ALIAS_FILE = "/etc/aliases" // aliases db path for query + 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 + MAX_VARIANCE = 50 // tries made to create login automatically + MIN_UID = 1000 + MAX_UID = 6000 ) var CreateUserCmd = &cobra.Command{ @@ -33,7 +33,7 @@ var CreateUserCmd = &cobra.Command{ func init() { // possible flags - // FIXME: maybe leave less flags for user input + // FIXME: maybe leave less flags for user input CreateUserCmd.Flags().StringP("grr", "r", "_", "User GRR, required for ini type") CreateUserCmd.Flags().StringP("type", "t", "ini", "Type of auto-generated login: ini, first or last") CreateUserCmd.Flags().StringP("path", "w", "", "Full path to webdir, /home/html/inf/login if empty") @@ -58,160 +58,171 @@ func init() { } func createUserFunc(cmd *cobra.Command, args []string) error { - success := false - // creates model from users input - usr, confirm, err := createNewUserModel(cmd) - if err != nil { return err } - - defer func() { - if !success { - _ = delKerberosPrincipal(usr.UID) - _ = delUserLDAP(usr) - } - }() - - // prints info for confirmation + success := false + // creates model from users input + usr, confirm, err := createNewUserModel(cmd) + if err != nil { + return err + } + + defer func() { + if !success { + _ = delKerberosPrincipal(usr.UID) + _ = delUserLDAP(usr) + } + }() + + // prints info for confirmation fmt.Printf("%v\n", usr.FullToString()) // for debug //fmt.Printf("%v\n Passwd: %v\n\n", usr.ToString(), usr.Password) - confirmationPrompt(confirm, "creation") + confirmationPrompt(confirm, "creation") - if usr.Password == "[auto-generate]" { - usr.Password = genPassword() - } + if usr.Password == "[auto-generate]" { + usr.Password = genPassword() + } - err = addUserLDAP(usr) - if err != nil { return err } + err = addUserLDAP(usr) + if err != nil { + return err + } - err = addKerberosPrincipal(usr.UID) - if err != nil { return err } + err = addKerberosPrincipal(usr.UID) + if err != nil { + return err + } - err = modKerberosPassword(usr.UID, usr.Password) - if err != nil { return err } + err = modKerberosPassword(usr.UID, usr.Password) + if err != nil { + return err + } - err = createUserDirs(usr) - if err != nil { return err } + err = createUserDirs(usr) + if err != nil { + return err + } fmt.Println("User created!") - fmt.Printf("\nUser Login: %v", usr.UID) - fmt.Printf("\nUser Password: %v\n\n", usr.Password) - fmt.Printf("%v:%v:%v:%v:%v:%v:%v:%v:%v:\n", usr.UID, usr.Password, usr.GID, - usr.Name, usr.Gecos, usr.GRR, usr.Shell, usr.Homedir, usr.Webdir) + fmt.Printf("\nUser Login: %v", usr.UID) + fmt.Printf("\nUser Password: %v\n\n", usr.Password) + fmt.Printf("%v:%v:%v:%v:%v:%v:%v:%v:%v:\n", usr.UID, usr.Password, usr.GID, + usr.Name, usr.Gecos, usr.GRR, usr.Shell, usr.Homedir, usr.Webdir) - success = true + success = true return nil } // creates and validates user inputs into the User model func createNewUserModel(cmd *cobra.Command) (model.User, bool, error) { - var u model.User - var opts model.Opts - users, err := getUsers() - if err != nil { - return u, false, err - } - - groups, err := getGroups() - if err != nil { - return u, false, err - } - - err = opts.RetrieveOpts(cmd) - if err != nil { - return u, false, err - } - - if err := validateInputs(opts) - err != nil { - return u, false, err - } - - if opts.Status == "Blocked" { - opts.Shell = "/bin/false" - } - - if opts.Ltype == "ini" && opts.GRR == "_" { - return u, false, fmt.Errorf("GRR is required for \"ini\" login type") - } - - if opts.UID == "" { - opts.UID, err = genUniqueUID(opts.Name, opts.GRR, opts.Ltype, users) - if err != nil { - return u, false, err - } - } - - opts.Homedir, err = genDirPath("/home", opts.GID, opts.UID, opts.Homedir) - if err != nil { - return u, false, err - } - - opts.Nobkp, err = genDirPath("/nobackup", opts.GID, opts.UID, opts.Nobkp) - if err != nil { - return u, false, err - } - - opts.Webdir, err = genDirPath("/home/html/inf", "", opts.UID, opts.Webdir) - if err != nil { - return u, false, err - } - - u = model.User{ - UID: opts.UID, - GID: opts.GID, - GRR: opts.GRR, - Resp: opts.Resp, - Name: opts.Name, - Ltype: opts.Ltype, - Shell: opts.Shell, - Status: opts.Status, - Expiry: opts.Expiry, - Course: opts.Course, - Webdir: opts.Webdir, - Homedir: opts.Homedir, - Nobackup: opts.Nobkp, - Password: ifThenElse(opts.Passwd != "", opts.Passwd, "[auto-generate]"), - } - - u = genGecos(u) - - // get a new UIDNumber - newUIDNumber, err := getNewUIDNumber() - if err != nil { - return u, false, fmt.Errorf("failed to generate new UIDNumber for user: %v", err) - } - u.UIDNumber = newUIDNumber - - // assign GIDNumber by traversing the groups - for key, val := range groups { - if val == u.GID { - u.GIDNumber = key - break - } - } - - u.DN = "uid=" + u.UID + ",ou=usuarios,dc=c3local" - - return u, opts.Confirm, nil + var u model.User + var opts model.Opts + users, err := getUsers() + if err != nil { + return u, false, err + } + + groups, err := getGroups() + if err != nil { + return u, false, err + } + + err = opts.RetrieveOpts(cmd) + if err != nil { + return u, false, err + } + + if err := validateInputs(opts); err != nil { + return u, false, err + } + + if opts.Status == "Blocked" { + opts.Shell = "/bin/false" + } + + if opts.Ltype == "ini" && opts.GRR == "_" { + return u, false, fmt.Errorf("GRR is required for \"ini\" login type") + } + + if opts.UID == "" { + opts.UID, err = genUniqueUID(opts.Name, opts.GRR, opts.Ltype, users) + if err != nil { + return u, false, err + } + } + + opts.Homedir, err = genDirPath("/home", opts.GID, opts.UID, opts.Homedir) + if err != nil { + return u, false, err + } + + opts.Nobkp, err = genDirPath("/nobackup", opts.GID, opts.UID, opts.Nobkp) + if err != nil { + return u, false, err + } + + opts.Webdir, err = genDirPath("/home/html/inf", "", opts.UID, opts.Webdir) + if err != nil { + return u, false, err + } + + u = model.User{ + UID: opts.UID, + GID: opts.GID, + GRR: opts.GRR, + Resp: opts.Resp, + Name: opts.Name, + Ltype: opts.Ltype, + Shell: opts.Shell, + Status: opts.Status, + Expiry: opts.Expiry, + Course: opts.Course, + Webdir: opts.Webdir, + Homedir: opts.Homedir, + Nobackup: opts.Nobkp, + Password: ifThenElse(opts.Passwd != "", opts.Passwd, "[auto-generate]"), + } + + u = genGecos(u) + + // get a new UIDNumber + newUIDNumber, err := getNewUIDNumber() + if err != nil { + return u, false, fmt.Errorf("failed to generate new UIDNumber for user: %v", err) + } + u.UIDNumber = newUIDNumber + + // assign GIDNumber by traversing the groups + for key, val := range groups { + if val == u.GID { + u.GIDNumber = key + break + } + } + + u.DN = "uid=" + u.UID + ",ou=usuarios,dc=c3local" + + return u, opts.Confirm, nil } func genDirPath(base, group, login, input string) (string, error) { - if input != "" { return input, nil } - p := filepath.Join(base, group, login) - return p, validatePath(p) + if input != "" { + return input, nil + } + p := filepath.Join(base, group, login) + return p, validatePath(p) } func genGecos(u model.User) model.User { - gecos := u.Name + "," - gecos += u.GRR + "," - gecos += u.Resp + "," - gecos += u.Course + "," - gecos += u.Status + "," - gecos += u.Expiry + "," - gecos += u.Ltype + "," - gecos += u.Webdir + "," - gecos += u.Nobackup - u.Gecos = gecos - return u + gecos := u.Name + "," + gecos += u.GRR + "," + gecos += u.Resp + "," + gecos += u.Course + "," + gecos += u.Status + "," + gecos += u.Expiry + "," + gecos += u.Ltype + "," + gecos += u.Webdir + "," + gecos += u.Nobackup + u.Gecos = gecos + return u } type LoginType int @@ -277,8 +288,8 @@ func genLogin(name string, grr string, ltype LoginType, variance int) string { // removes connectives, leave all lowecase and splits the name func formatName(name string) []string { - connectives := map[string]bool{"da": true, "de": true, "di": true, - "do": true, "das": true, "dos": true, "von": true} + connectives := map[string]bool{"da": true, "de": true, "di": true, + "do": true, "das": true, "dos": true, "von": true} splitName := strings.Fields(name) var parts []string @@ -288,13 +299,13 @@ func formatName(name string) []string { parts = append(parts, string(lowerPart)) } } - return parts + return parts } func genUniqueUID(name, grr string, ltypeString string, users []model.User) (string, error) { - var uid string - used, variance := true, 0 - for used { + var uid string + used, variance := true, 0 + for used { var ltype LoginType if ltypeString == "ini" { ltype = Initials @@ -303,73 +314,73 @@ func genUniqueUID(name, grr string, ltypeString string, users []model.User) (str } else { ltype = LastName } - uid = genLogin(name, grr, ltype, variance) + uid = genLogin(name, grr, ltype, variance) - // already taken or alias for it exists :( - used = loginExists(users, uid) || mailAliasExists(uid) + // already taken or alias for it exists :( + used = loginExists(users, uid) || mailAliasExists(uid) - variance++ - if variance > MAX_VARIANCE { - return "", fmt.Errorf("Could't generate login automatically, please inform the desired login\n") - } - } - return uid, nil + variance++ + if variance > MAX_VARIANCE { + return "", fmt.Errorf("Could't generate login automatically, please inform the desired login\n") + } + } + return uid, nil } // queries to check if the alias exists func mailAliasExists(alias string) bool { - cmd := exec.Command("/usr/sbin/postalias", "-q", alias, MAIL_ALIAS_FILE) - cmd.Stdout = nil - cmd.Stderr = nil - return cmd.Run() == nil -} + cmd := exec.Command("/usr/sbin/postalias", "-q", alias, MAIL_ALIAS_FILE) + cmd.Stdout = nil + cmd.Stderr = nil + return cmd.Run() == nil +} // finds next available uidNumber (MEX from uid group) func getNewUIDNumber() (string, error) { - uids, err := getUIDs() - if err != nil { - return "", err - } - - candidate := MIN_UID - for _, uid := range uids { - if uid == candidate { // check if taken - candidate++ - } else if uid > candidate { // found a gap - break - } - } - - if candidate > MAX_UID { - return "", fmt.Errorf("No more available UID numbers") - } - return strconv.Itoa(candidate), nil + uids, err := getUIDs() + if err != nil { + return "", err + } + + candidate := MIN_UID + for _, uid := range uids { + if uid == candidate { // check if taken + candidate++ + } else if uid > candidate { // found a gap + break + } + } + + if candidate > MAX_UID { + return "", fmt.Errorf("No more available UID numbers") + } + return strconv.Itoa(candidate), nil } // generates a LDAP request and adds the user to LDAP func addUserLDAP(u model.User) error { - l, err := connLDAP() - if err != nil { - return err - } - defer l.Close() + l, err := connLDAP() + if err != nil { + return err + } + defer l.Close() req := ldap.NewAddRequest(u.DN, nil) req.Attribute("uid", []string{u.UID}) req.Attribute("cn", []string{u.Name}) req.Attribute("objectClass", []string{"account", "posixAccount"}) - req.Attribute("loginShell", []string{u.Shell}) - req.Attribute("uidNumber", []string{u.UIDNumber}) - req.Attribute("gidNumber", []string{u.GIDNumber}) + req.Attribute("loginShell", []string{u.Shell}) + req.Attribute("uidNumber", []string{u.UIDNumber}) + req.Attribute("gidNumber", []string{u.GIDNumber}) req.Attribute("homeDirectory", []string{u.Homedir}) req.Attribute("gecos", []string{u.Gecos}) - err = l.Add(req) - if err != nil { - return fmt.Errorf("Failed to add user %s to LDAP: %v", u.UID, err) - } + err = l.Add(req) + if err != nil { + return fmt.Errorf("Failed to add user %s to LDAP: %v", u.UID, err) + } - return nil + return nil } // creates a KerberosPrincipal for the user @@ -379,7 +390,7 @@ func addKerberosPrincipal(login string) error { output, err := cmd.CombinedOutput() if err != nil { - return fmt.Errorf("Failed to add Kerberos principal: %v\nOutput: %s", err, output) + return fmt.Errorf("Failed to add Kerberos principal: %v\nOutput: %s", err, output) } return nil @@ -390,8 +401,8 @@ func modKerberosPassword(login, password string) error { cmd := exec.Command("kadmin.local", "-q", fmt.Sprintf("cpw -pw %s %s", password, login)) - cmd.Stdout = nil - cmd.Stderr = nil + cmd.Stdout = nil + cmd.Stderr = nil output, err := cmd.CombinedOutput() if err != nil { return fmt.Errorf("Failed to change password for %s: %v\nOutput: %s", login, err, output) @@ -401,41 +412,43 @@ func modKerberosPassword(login, password string) error { } func createUserDirs(u model.User) error { - success := false + success := false - defer func() { - if !success { - fmt.Println("Error found creating dirs, cleaning up...") - _ = os.RemoveAll(u.Nobackup) - _ = os.RemoveAll(u.Homedir) - } - }() + defer func() { + if !success { + fmt.Println("Error found creating dirs, cleaning up...") + _ = os.RemoveAll(u.Nobackup) + _ = os.RemoveAll(u.Homedir) + } + }() - if err := createHome(u, u.Homedir) - err != nil { return err } + if err := createHome(u, u.Homedir); err != nil { + return err + } - if err := createHome(u, u.Nobackup) - err != nil { return err } + if err := createHome(u, u.Nobackup); err != nil { + return err + } - if err := createWeb(u) - err != nil { return err } + if err := createWeb(u); err != nil { + return err + } - success = true - return nil + success = true + return nil } - func createHome(u model.User, homeDir string) error { - perm := DEF_PERMISSION - if u.Status == "Blocked" { - perm = BLK_PERMISSION - } + perm := DEF_PERMISSION + if u.Status == "Blocked" { + perm = BLK_PERMISSION + } // create directory cmd := exec.Command("mkdir", "-p", homeDir) - cmd.Stdout = nil - cmd.Stderr = nil + cmd.Stdout = nil + cmd.Stderr = nil if err := cmd.Run(); err != nil { return fmt.Errorf("Failed to create home directory: %w", err) } @@ -447,39 +460,39 @@ func createHome(u model.User, homeDir string) error { } // change permissions - cmd = exec.Command("chmod", perm, homeDir) + cmd = exec.Command("chmod", perm, homeDir) if err := cmd.Run(); err != nil { return fmt.Errorf("Failed to set permissions: %w", err) } // change ownership - cmd = exec.Command("chown", "-R", fmt.Sprintf("%s:%s", u.UID, u.GID), homeDir) + cmd = exec.Command("chown", "-R", fmt.Sprintf("%s:%s", u.UID, u.GID), homeDir) if err := cmd.Run(); err != nil { return fmt.Errorf("Failed to change ownership: %w", err) } - return nil + return nil } func createWeb(u model.User) error { - success := false + success := false - perm := DEF_PERMISSION - if u.Status == "Blocked" { - perm = BLK_PERMISSION - } + perm := DEF_PERMISSION + if u.Status == "Blocked" { + perm = BLK_PERMISSION + } - defer func() { - if !success { - _ = os.RemoveAll(u.Webdir) - } - }() + defer func() { + if !success { + _ = os.RemoveAll(u.Webdir) + } + }() // create directory cmd := exec.Command("mkdir", "-p", u.Webdir) - cmd.Stdout = nil - cmd.Stderr = nil - if err := cmd.Run(); err != nil { + cmd.Stdout = nil + cmd.Stderr = nil + if err := cmd.Run(); err != nil { return fmt.Errorf("Failed to create web directory: %w", err) } @@ -489,57 +502,61 @@ func createWeb(u model.User) error { return fmt.Errorf("Failed to create index.html: %w", err) } - // create link in users home + // create link in users home cmd = exec.Command("ln", "-s", u.Webdir, filepath.Join(u.Homedir, "public_html")) if err := cmd.Run(); err != nil { return fmt.Errorf("Failed to create link public_html: %w", err) } // change permissions - cmd = exec.Command("chmod", perm, u.Webdir) + cmd = exec.Command("chmod", perm, u.Webdir) if err := cmd.Run(); err != nil { return fmt.Errorf("Failed to set permissions: %w", err) } // change ownership - cmd = exec.Command("chown", "-R", fmt.Sprintf("%s:%s", u.UID, u.GID), u.Webdir) + cmd = exec.Command("chown", "-R", fmt.Sprintf("%s:%s", u.UID, u.GID), u.Webdir) if err := cmd.Run(); err != nil { return fmt.Errorf("Failed to change ownership: %w", err) } - // set permission for public_html + // set permission for public_html cmd = exec.Command("chmod", HTML_BASE_PERM, filepath.Join(u.Homedir, "public_html")) if err := cmd.Run(); err != nil { return fmt.Errorf("Failed to create set permissions for public_html: %w", err) } - success = true - return nil + success = true + return nil } func validatePath(path string) error { - _, err := os.Stat(path) - if os.IsNotExist(err) { return nil } - return fmt.Errorf("Path \"%v\" already exists, please provide a new path", path) + _, err := os.Stat(path) + if os.IsNotExist(err) { + return nil + } + return fmt.Errorf("Path \"%v\" already exists, please provide a new path", path) } func validateExpiry(expiry string) error { - if expiry == "_" { return nil } - - parts := strings.Split(expiry, ".") - if !isValidDate(parts) { - err := fmt.Errorf("Malformed expiry date string, use \"dd.mm.yy\"") - return err - } - return nil + if expiry == "_" { + return nil + } + + parts := strings.Split(expiry, ".") + if !isValidDate(parts) { + err := fmt.Errorf("Malformed expiry date string, use \"dd.mm.yy\"") + return err + } + return nil } func validateStatus(status string) error { - if status != "Blocked" && status != "Active" { - err := fmt.Errorf("User status can only be \"Active\" or \"Blocked\"") - return err - } - return nil + if status != "Blocked" && status != "Active" { + err := fmt.Errorf("User status can only be \"Active\" or \"Blocked\"") + return err + } + return nil } func validateLtype(ltype string) error { @@ -551,80 +568,98 @@ func validateLtype(ltype string) error { } func validateGRR(grr string) error { - // OK if empty, only "ini" login type requires it and we check :) - if grr == "_" { return nil } + // OK if empty, only "ini" login type requires it and we check :) + if grr == "_" { + return nil + } - users, err := getUsers() - if err != nil { return err } + users, err := getUsers() + if err != nil { + return err + } - isValid, _ := regexp.MatchString(`^\d{8}$`, grr) // is 8 digit number + isValid, _ := regexp.MatchString(`^\d{8}$`, grr) // is 8 digit number if !isValid { - err := fmt.Errorf("Malformed GRR string, must be 8 digit number") + err := fmt.Errorf("Malformed GRR string, must be 8 digit number") return err } - if grrExists(users, grr) { - err := fmt.Errorf(`The informed GRR already exists in LDAP database + if grrExists(users, grr) { + err := fmt.Errorf(`The informed GRR already exists in LDAP database Note: To search for the account use "useradm user show -r %s"`, grr) - return err - } + return err + } - return nil + return nil } func validateGID(group string) error { - var err error - - groups, err := getGroups() - if err != nil { return err } - - for _, value := range groups { - if value == group { - return nil - } - } - err = fmt.Errorf("Could't find group \"%v\" in LDAP database", group) - return err + var err error + + groups, err := getGroups() + if err != nil { + return err + } + + for _, value := range groups { + if value == group { + return nil + } + } + err = fmt.Errorf("Could't find group \"%v\" in LDAP database", group) + return err } func validateUID(login string) error { - users, err := getUsers() - if err != nil { return err } - res := searchUser(users, false, login, "", "", "", "", "") - if len(res) != 0 { - return fmt.Errorf(`The informed Login already exists in LDAP database + users, err := getUsers() + if err != nil { + return err + } + res := searchUser(users, false, 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) - } - return nil + } + return nil } func validateInputs(opts model.Opts) error { - var err error + var err error - err = validateGID(opts.GID) - if err != nil { return err } + err = validateGID(opts.GID) + if err != nil { + return err + } - err = validateGRR(opts.GRR) - if err != nil { return err } + err = validateGRR(opts.GRR) + if err != nil { + return err + } - err = validateExpiry(opts.Expiry) - if err != nil { return err } + err = validateExpiry(opts.Expiry) + if err != nil { + return err + } - err = validateStatus(opts.Status) - if err != nil { return err } + err = validateStatus(opts.Status) + if err != nil { + return err + } err = validateLtype(opts.Ltype) if err != nil { return err } - // it’s OK if UID is empty here, we generate it later :) - if opts.UID != "" { - err := validateUID(opts.UID) - if err != nil { return err } - } + // it’s OK if UID is empty here, we generate it later :) + if opts.UID != "" { + err := validateUID(opts.UID) + if err != nil { + return err + } + } - return nil + return nil } func isValidDate(arr []string) bool { @@ -633,8 +668,8 @@ func isValidDate(arr []string) bool { } // convert to int - day, err1 := strconv.Atoi(arr[0]) - mth, err2 := strconv.Atoi(arr[1]) + day, err1 := strconv.Atoi(arr[0]) + mth, err2 := strconv.Atoi(arr[1]) year, err3 := strconv.Atoi(arr[2]) if err1 != nil || err2 != nil || err3 != nil { @@ -653,6 +688,8 @@ func isValidDate(arr []string) bool { } func ifThenElse(condition bool, a string, b string) string { - if condition { return a } + if condition { + return a + } return b } diff --git a/cmd/user/delete.go b/cmd/user/delete.go index 07a0340d61402611802ed5a3e3d3a086cd219e81..d39b84d640b77999d3f3687142ddc9038d34e7c3 100644 --- a/cmd/user/delete.go +++ b/cmd/user/delete.go @@ -1,179 +1,192 @@ package user import ( - "os" "fmt" "log" - "time" + "os" "os/exec" - "strconv" "path/filepath" + "strconv" + "time" - "github.com/spf13/cobra" "github.com/go-ldap/ldap/v3" + "github.com/spf13/cobra" "gitlab.c3sl.ufpr.br/tss24/useradm/model" ) - var ( - ANO = strconv.Itoa(time.Now().Year()) - NO_BKP_TRASH = "/nobackup/contas_removidas/" + ANO - HOME_TRASH = "/home/contas_removidas/" + ANO - WEB_TRASH = "/home/contas_removidas/html/" + ANO + ANO = strconv.Itoa(time.Now().Year()) + NO_BKP_TRASH = "/nobackup/contas_removidas/" + ANO + HOME_TRASH = "/home/contas_removidas/" + ANO + WEB_TRASH = "/home/contas_removidas/html/" + ANO ) var DeleteUserCmd = &cobra.Command{ Use: "delete", Short: "Delete a user", - RunE: deleteUserFunc, + RunE: deleteUserFunc, } func init() { - DeleteUserCmd.Flags().StringP("login", "l", "", "User login for removal") - DeleteUserCmd.Flags().BoolP("confirm", "y", false, "Skip confirmation prompt") + DeleteUserCmd.Flags().StringP("login", "l", "", "User login for removal") + DeleteUserCmd.Flags().BoolP("confirm", "y", false, "Skip confirmation prompt") - DeleteUserCmd.MarkFlagRequired("login") + DeleteUserCmd.MarkFlagRequired("login") } func deleteUserFunc(cmd *cobra.Command, args []string) error { - var opts model.Opts - success := false + var opts model.Opts + success := false - err := opts.RetrieveOpts(cmd) - if err != nil { return err } - - u, err := locateUser(opts.UID) - if err != nil { return err } - - defer func() { - if !success { - log.Println("Found error, rolling back dirs...") - _ = moveAndChown(filepath.Join(NO_BKP_TRASH, filepath.Base(u.Nobackup)), - u.Nobackup, u.UID, u.GID) - _ = moveAndChown(filepath.Join(HOME_TRASH, filepath.Base(u.Homedir)), - u.Homedir, u.UID, u.GID) - _ = moveAndChown(filepath.Join(WEB_TRASH, filepath.Base(u.Webdir)), - u.Webdir, u.UID, u.GID) - } - }() - - fmt.Printf("Found %v\n\n", u.ToString()) + err := opts.RetrieveOpts(cmd) + if err != nil { + return err + } - confirmationPrompt(opts.Confirm, "removal") + u, err := locateUser(opts.UID) + if err != nil { + return err + } - err = removeDirs(u) - if err != nil { return err } + defer func() { + if !success { + log.Println("Found error, rolling back dirs...") + _ = moveAndChown(filepath.Join(NO_BKP_TRASH, filepath.Base(u.Nobackup)), + u.Nobackup, u.UID, u.GID) + _ = moveAndChown(filepath.Join(HOME_TRASH, filepath.Base(u.Homedir)), + u.Homedir, u.UID, u.GID) + _ = moveAndChown(filepath.Join(WEB_TRASH, filepath.Base(u.Webdir)), + u.Webdir, u.UID, u.GID) + } + }() + + fmt.Printf("Found %v\n\n", u.ToString()) + + confirmationPrompt(opts.Confirm, "removal") + + err = removeDirs(u) + if err != nil { + return err + } - err = delUserLDAP(u) - if err != nil { return err } + err = delUserLDAP(u) + if err != nil { + return err + } - fmt.Printf("\nUser removed!\n") + fmt.Printf("\nUser removed!\n") - success = true - return nil + success = true + return nil } // searches for the specified login string, there can only be one >:) func locateUser(login string) (model.User, error) { - var u model.User - users, err := getUsers() + var u model.User + users, err := getUsers() - if !loginExists(users, login) { - return u, fmt.Errorf("Failed to find login in LDAP database: %v", err) - } + if !loginExists(users, login) { + return u, fmt.Errorf("Failed to find login in LDAP database: %v", err) + } - filter := searchUser(users, false, login, "", "", "", "", "") - if len(filter) != 1 { - return u, fmt.Errorf(`More than one user matched the login given. + filter := searchUser(users, false, 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) - } + } - u = filter[0] - return u, nil + u = filter[0] + return u, nil } // moves dirs to their respective trash dir func removeDirs(u model.User) error { - err := moveAndChown(u.Homedir, HOME_TRASH, "nobody", "nogroup") - if err != nil { return err } + err := moveAndChown(u.Homedir, HOME_TRASH, "nobody", "nogroup") + if err != nil { + return err + } - err = moveAndChown(u.Nobackup, NO_BKP_TRASH, "nobody", "nogroup") - if err != nil { return err } + err = moveAndChown(u.Nobackup, NO_BKP_TRASH, "nobody", "nogroup") + if err != nil { + return err + } - err = moveAndChown(u.Webdir, WEB_TRASH, "nobody", "nogroup") - if err != nil { return err } + err = moveAndChown(u.Webdir, WEB_TRASH, "nobody", "nogroup") + if err != nil { + return err + } - return nil + return nil } func delUserLDAP(u model.User) error { - l, err := connLDAP() - if err != nil { - return err - } - defer l.Close() - - // first remove user from all groups - 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"}, - 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 - 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) - } - } - - // removing user entry - 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 nil + l, err := connLDAP() + if err != nil { + return err + } + defer l.Close() + + // first remove user from all groups + 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"}, + 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 + 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) + } + } + + // removing user entry + 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 nil } // usually not necessary but used in a failed create command func delKerberosPrincipal(login string) error { - cmd := exec.Command("kadmin.local", "-q", fmt.Sprintf("delprinc -force %s", login)) + cmd := exec.Command("kadmin.local", "-q", fmt.Sprintf("delprinc -force %s", login)) - output, err := cmd.CombinedOutput() - if err != nil { - return fmt.Errorf("Fail to delete Kerberos principal: %v\nOutput: %s", err, output) - } + output, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("Fail to delete Kerberos principal: %v\nOutput: %s", err, output) + } - return nil + return nil } func moveAndChown(orig, dest, owner, group string) error { - // check if orig exists - if _, err := os.Stat(orig); err != nil { - log.Printf("Directory %v not found so not moved\n", orig) - return nil - } + // check if orig exists + if _, err := os.Stat(orig); err != nil { + log.Printf("Directory %v not found so not moved\n", orig) + return nil + } // construct destination path destPath := filepath.Join(dest, filepath.Base(orig)) - if _, err := os.Stat(destPath); err == nil { - return fmt.Errorf("Directory %v already exists, can't move\n", destPath) - } + if _, err := os.Stat(destPath); err == nil { + return fmt.Errorf("Directory %v already exists, can't move\n", destPath) + } // move directory cmd := exec.Command("mv", orig, destPath) @@ -182,11 +195,10 @@ func moveAndChown(orig, dest, owner, group string) error { } // recursive chown - cmd = exec.Command("chown", "-R", owner+":"+group, destPath) + cmd = exec.Command("chown", "-R", owner+":"+group, destPath) if output, err := cmd.CombinedOutput(); err != nil { return fmt.Errorf("Failed to set owner/group: %w\nOutput: %v", err, output) } return nil } - diff --git a/cmd/user/mod.go b/cmd/user/mod.go index 5cfd58a24f0a6f282aa2bf02dab518857334d515..74f23bbd96101cc57ca086237eb303d6f309692b 100644 --- a/cmd/user/mod.go +++ b/cmd/user/mod.go @@ -1,66 +1,66 @@ package user import ( - "os" - "fmt" - "bufio" - "strings" - "os/exec" - "crypto/rand" - + "bufio" + "crypto/rand" + "fmt" + "os" + "os/exec" + "strings" + + "github.com/spf13/cobra" + "gitlab.c3sl.ufpr.br/tss24/useradm/model" "gopkg.in/yaml.v3" - "github.com/spf13/cobra" - "gitlab.c3sl.ufpr.br/tss24/useradm/model" ) type cfg struct { - Name string `yaml:"name"` - GRR string `yaml:"grr"` - Group string `yaml:"group"` - Status string `yaml:"status"` - Shell string `yaml:"shell"` - Course string `yaml:"course"` - Resp string `yaml:"resp"` - Expiry string `yaml:"expiry"` + Name string `yaml:"name"` + GRR string `yaml:"grr"` + Group string `yaml:"group"` + Status string `yaml:"status"` + Shell string `yaml:"shell"` + Course string `yaml:"course"` + Resp string `yaml:"resp"` + Expiry string `yaml:"expiry"` } var ModCmd = &cobra.Command{ Use: "mod [username]", Short: "Modify user information", - Args: cobra.ExactArgs(1), - RunE: modifyUserFunc, + Args: cobra.ExactArgs(1), + RunE: modifyUserFunc, } func modifyUserFunc(cmd *cobra.Command, args []string) error { - var users []model.User - users, err := getUsers() - if err != nil { - return err - } - - login := args[0] - res := searchUser(users, false, login, "", "", "", "", "") - if len(res) >= 1 { - err = fmt.Errorf("More than one user found") - return err - } - u := res[0] - - state := cfg{ - Name: u.Name, - GRR: u.GRR, - Group: u.GID, - Status: u.Status, - Shell: u.Shell, - Course: u.Course, - Resp: u.Resp, - Expiry: u.Expiry, - } - - // Criar arquivo temporário + var users []model.User + users, err := getUsers() + if err != nil { + return err + } + + login := args[0] + res := searchUser(users, false, login, "", "", "", "", "") + if len(res) != 1 { + err = fmt.Errorf("More than one user found") + return err + } + u := res[0] + + state := cfg{ + Name: u.Name, + GRR: u.GRR, + Group: u.GID, + Status: u.Status, + Shell: u.Shell, + Course: u.Course, + Resp: u.Resp, + Expiry: u.Expiry, + } + + // Criar arquivo temporário tmpFile, err := os.CreateTemp("", "config-*.yaml") if err != nil { - err = fmt.Errorf("Error trying to create temp file:", err) + err = fmt.Errorf("Error trying to create temp file: %v", err) return err } defer os.Remove(tmpFile.Name()) @@ -68,13 +68,13 @@ func modifyUserFunc(cmd *cobra.Command, args []string) error { // Serializar apenas os campos editáveis data, err := yaml.Marshal(state) if err != nil { - err = fmt.Errorf("Error serializing the yaml:", err) + err = fmt.Errorf("Error serializing the yaml: %v", err) return err } // Escrever no arquivo temporário if err := os.WriteFile(tmpFile.Name(), data, 0644); err != nil { - err = fmt.Errorf("Error writing to temp file:", err) + err = fmt.Errorf("Error writing to temp file: %v", err) return err } @@ -90,21 +90,21 @@ func modifyUserFunc(cmd *cobra.Command, args []string) error { comd.Stderr = os.Stderr if err := comd.Run(); err != nil { - err = fmt.Errorf("Error opening the editor:", err) + err = fmt.Errorf("Error opening the editor: %v", err) return err } // Ler o conteúdo editado editedData, err := os.ReadFile(tmpFile.Name()) if err != nil { - err = fmt.Errorf("Error reading the modified file:", err) + err = fmt.Errorf("Error reading the modified file: %v", err) return err } // Desserializar apenas os campos editáveis var newState cfg if err := yaml.Unmarshal(editedData, &newState); err != nil { - err = fmt.Errorf("Error de-serializing the yaml:", err) + err = fmt.Errorf("Error de-serializing the yaml: %v", err) return err } @@ -112,33 +112,33 @@ func modifyUserFunc(cmd *cobra.Command, args []string) error { fmt.Println("New config:") fmt.Printf("%+v\n", newState) - return nil + return nil } // do NOT include ':' in the charset, it WILL break the command func genPassword() string { - const charset = "@*()=+[];,.?123456789abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ" - b := make([]byte, 20) - if _, err := rand.Read(b); err != nil { - panic(err) - } - for i := range b { - b[i] = charset[int(b[i])%len(charset)] - } - return string(b) + const charset = "@*()=+[];,.?123456789abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ" + b := make([]byte, 20) + if _, err := rand.Read(b); err != nil { + panic(err) + } + for i := range b { + b[i] = charset[int(b[i])%len(charset)] + } + return string(b) } // 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) + if !confirm { + fmt.Printf("Proceed with user %v? [y/N] ", operation) - reader := bufio.NewReader(os.Stdin) - response, _ := reader.ReadString('\n') + reader := bufio.NewReader(os.Stdin) + response, _ := reader.ReadString('\n') - if strings.TrimSpace(strings.ToLower(response)) != "y" { - fmt.Fprintln(os.Stderr, "Aborted.") - os.Exit(1) - } - } + if strings.TrimSpace(strings.ToLower(response)) != "y" { + fmt.Fprintln(os.Stderr, "Aborted.") + os.Exit(1) + } + } } diff --git a/cmd/user/show.go b/cmd/user/show.go index 277a74a2a98bab5c5a96e214ff54ee880f30b2a1..882705b431c87a080bc5c4b92f8f4f1ff7d3e269 100644 --- a/cmd/user/show.go +++ b/cmd/user/show.go @@ -1,219 +1,208 @@ package user import ( - "os" "fmt" + "os" "sort" "strconv" "strings" - "github.com/spf13/cobra" "github.com/go-ldap/ldap/v3" + "github.com/spf13/cobra" "gitlab.c3sl.ufpr.br/tss24/useradm/model" ) const ( - NUM_GECOS_FIELDS = 9 // change if going to add new field - PASSWD_PATH = "/etc/ldapscripts/ldapscripts.passwd" + NUM_GECOS_FIELDS = 9 // change if going to add new field + PASSWD_PATH = "/etc/ldapscripts/ldapscripts.passwd" ) var ShowCmd = &cobra.Command{ Use: "show", Short: "Search for users and show info", - RunE: searchUserFunc, + RunE: searchUserFunc, } func init() { - // possible search fields - ShowCmd.Flags().StringP("grr", "r", "", "Search by user GRR") - ShowCmd.Flags().StringP("name", "n", "", "Search by user name (case sensitive)") - ShowCmd.Flags().StringP("login", "l", "", "Search by login") - ShowCmd.Flags().StringP("group", "g", "", "Search by user base group") - ShowCmd.Flags().StringP("status", "s", "", "Search by user status (Active/Blocked)") - ShowCmd.Flags().StringP("homedir", "d", "", "Search by user homedir") - - // at least one is required! - ShowCmd.MarkFlagsOneRequired("grr", "name", "login", "group", "homedir", "status") - - ShowCmd.Flags().BoolP("ignore", "i", false, "Make the search case-insensitive") + // possible search fields + ShowCmd.Flags().StringP("grr", "r", "", "Search by user GRR") + ShowCmd.Flags().StringP("name", "n", "", "Search by user name (case sensitive)") + ShowCmd.Flags().StringP("login", "l", "", "Search by login") + ShowCmd.Flags().StringP("group", "g", "", "Search by user base group") + ShowCmd.Flags().StringP("status", "s", "", "Search by user status (Active/Blocked)") + ShowCmd.Flags().StringP("homedir", "d", "", "Search by user homedir") + + // at least one is required! + ShowCmd.MarkFlagsOneRequired("grr", "name", "login", "group", "homedir", "status") + + ShowCmd.Flags().BoolP("ignore", "i", false, "Make the search case-insensitive") } func searchUserFunc(cmd *cobra.Command, args []string) error { - grr, err := getFlagString(cmd,"grr") - if err != nil { return err } - - name, err := getFlagString(cmd,"name") - if err != nil { return err } - - login, err := getFlagString(cmd,"login") - if err != nil { return err } - - group, err := getFlagString(cmd,"group") - if err != nil { return err } - - homedir, err := getFlagString(cmd,"homedir") - if err != nil { return err } - - status, err := getFlagString(cmd,"status") - if err != nil { return err } + var o model.Opts - ig, err := cmd.Flags().GetBool("ignore") - if err != nil { return err } + users, err := getUsers() + if err != nil { + return err + } - users, err := getUsers() - if err != nil { return err } + err = o.RetrieveOpts(cmd) + if err != nil { + return err + } - filtered := searchUser(users, ig, login, group, name, grr, status, homedir) + filtered := searchUser(users, o.Ignore, o.UID, o.GID, + o.Name, o.GRR, o.Status, o.Homedir) - for i := range filtered { - fmt.Printf("%v\n\n",filtered[i].ToString()) - } + for i := range filtered { + fmt.Printf("%v\n\n", filtered[i].ToString()) + } - return nil + return nil } func searchUser(users []model.User, ig bool, l, g, n, r, s, h string) []model.User { - return Filter(users, func(u model.User) bool { - contains := func(src, substr string) bool { - if ig { - return strings.Contains( // normalize if set to ignore - strings.ToLower(src), - strings.ToLower(substr), - ) - } - return strings.Contains(src, substr) // normal search - } - - // 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)) - }) + return Filter(users, func(u model.User) bool { + contains := func(src, substr string) bool { + if ig { + return strings.Contains( // normalize if set to ignore + strings.ToLower(src), + strings.ToLower(substr), + ) + } + return strings.Contains(src, substr) // normal search + } + + // 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)) + }) } // generic function for filtering data types, in this case the model User func Filter[T any](slice []T, predicate func(T) bool) []T { - var result []T - for _, v := range slice { - if predicate(v) { - result = append(result, v) - } - } - return result + var result []T + for _, v := range slice { + if predicate(v) { + result = append(result, v) + } + } + return result } func getUsers() ([]model.User, error) { - var users []model.User - - l, err := connLDAP() - if err != nil { - return users, err - } - defer l.Close() - - // create the LDAP search request - searchRequest := ldap.NewSearchRequest( - "dc=c3local", // search base - ldap.ScopeWholeSubtree, // scope - ldap.NeverDerefAliases, // aliases - 0, // size limit - 0, // time limit - false, // types only - "(objectClass=posixAccount)",// filter - []string{"dn", "uid", "cn", "loginShell", "uidNumber", - "gidNumber", "homeDirectory", "gecos"}, // attributes to return - nil, - ) - - // perform the search - sr, err := l.Search(searchRequest) - if err != nil { - err = fmt.Errorf("Failed to fetch users from LDAP: %v", err) - return nil, err - } - - // get all the groups and ids - groups, err := getGroups() - if err != nil { - err = fmt.Errorf("Failed to fetch groups and gids from LDAP: %v", err) - return nil, err - } - - // iterate over the search results - for _, entry := range sr.Entries { - shell := entry.GetAttributeValue("loginShell") - - user := model.User{ - DN: entry.DN, - UID: entry.GetAttributeValue("uid"), - Name: entry.GetAttributeValue("cn"), - Shell: shell, - UIDNumber: entry.GetAttributeValue("uidNumber"), - GIDNumber: entry.GetAttributeValue("gidNumber"), - Homedir: entry.GetAttributeValue("homeDirectory"), - Gecos: entry.GetAttributeValue("gecos"), - Status: ifThenElse(shell == "/bin/bash", "Active", "Blocked"), - } - - user.GID = groups[user.GIDNumber] - - gidNumber := user.GIDNumber - - // safe assignment :) - groupName, exists := groups[gidNumber] - if !exists { - fmt.Printf("WARNING: no group found for GIDNumber %s, user %s. Continuing...", gidNumber, user.UID) - } else { - user.GID = groupName - } - - user = parseGecos(user) - users = append(users, user) - } - - return users, nil + var users []model.User + + l, err := connLDAP() + if err != nil { + return users, err + } + defer l.Close() + + // create the LDAP search request + searchRequest := ldap.NewSearchRequest( + "dc=c3local", // search base + ldap.ScopeWholeSubtree, // scope + ldap.NeverDerefAliases, // aliases + 0, // size limit + 0, // time limit + false, // types only + "(objectClass=posixAccount)", // filter + []string{"dn", "uid", "cn", "loginShell", "uidNumber", + "gidNumber", "homeDirectory", "gecos"}, // attributes to return + nil, + ) + + // perform the search + sr, err := l.Search(searchRequest) + if err != nil { + err = fmt.Errorf("Failed to fetch users from LDAP: %v", err) + return nil, err + } + + // get all the groups and ids + groups, err := getGroups() + if err != nil { + err = fmt.Errorf("Failed to fetch groups and gids from LDAP: %v", err) + return nil, err + } + + // iterate over the search results + for _, entry := range sr.Entries { + shell := entry.GetAttributeValue("loginShell") + + user := model.User{ + DN: entry.DN, + UID: entry.GetAttributeValue("uid"), + Name: entry.GetAttributeValue("cn"), + Shell: shell, + UIDNumber: entry.GetAttributeValue("uidNumber"), + GIDNumber: entry.GetAttributeValue("gidNumber"), + Homedir: entry.GetAttributeValue("homeDirectory"), + Gecos: entry.GetAttributeValue("gecos"), + Status: ifThenElse(shell == "/bin/bash", "Active", "Blocked"), + } + + user.GID = groups[user.GIDNumber] + + gidNumber := user.GIDNumber + + // safe assignment :) + groupName, exists := groups[gidNumber] + if !exists { + fmt.Printf("WARNING: no group found for GIDNumber %s, user %s. Continuing...", gidNumber, user.UID) + } else { + user.GID = groupName + } + + user = parseGecos(user) + users = append(users, user) + } + + return users, nil } func connLDAP() (*ldap.Conn, error) { - // connect to the LDAP server - l, err := ldap.DialURL("ldapi:///") - if err != nil { - err = fmt.Errorf("Failed to connect to LDAP: %v", err) - return nil, err - } - - // get admin credentials - password, err := getLDAPPassword(PASSWD_PATH) - if err != nil { - err = fmt.Errorf("Failed to read LDAP password: %v", err) - return nil, err - } - - // bind using admin credentials - err = l.Bind("cn=admin,dc=c3local", password) - if err != nil { - l.Close() - err = fmt.Errorf("Failed to bind with credentials: %v", err) - return nil, err - } - - return l, nil + // connect to the LDAP server + l, err := ldap.DialURL("ldapi:///") + if err != nil { + err = fmt.Errorf("Failed to connect to LDAP: %v", err) + return nil, err + } + + // get admin credentials + password, err := getLDAPPassword(PASSWD_PATH) + if err != nil { + err = fmt.Errorf("Failed to read LDAP password: %v", err) + return nil, err + } + + // bind using admin credentials + err = l.Bind("cn=admin,dc=c3local", password) + if err != nil { + l.Close() + err = fmt.Errorf("Failed to bind with credentials: %v", err) + return nil, err + } + + return l, nil } func getLDAPPassword(path string) (string, error) { - passwd, err := os.ReadFile(path) - if err != nil { - err = fmt.Errorf("Error reading password file: %w", err) - return "", err - } + passwd, err := os.ReadFile(path) + if err != nil { + err = fmt.Errorf("Error reading password file: %w", err) + return "", err + } - password := strings.TrimSpace(string(passwd)) + password := strings.TrimSpace(string(passwd)) - return password, nil + return password, nil } // Gecos Fields: @@ -228,131 +217,131 @@ func getLDAPPassword(path string) (string, error) { // 8. users nobackup dir // 9. to be continued... func parseGecos(user model.User) model.User { - result := [NUM_GECOS_FIELDS]string{0: "_"} - for i := range result { - result[i] = "_" - } - parts := strings.Split(user.Gecos, ",") - - for i := 0; i < len(parts) && i < NUM_GECOS_FIELDS; i++ { - if strings.TrimSpace(parts[i]) != "" { - result[i] = parts[i] - } - } - - user.GRR = result[1] - user.Resp = result[2] - user.Course = result[3] - user.Expiry = result[4] - // only set with gecos if empty - if user.Status == "_"{ - user.Status = result[5] - } - user.Ltype = result[6] - user.Webdir = result[7] - user.Nobackup = result[8] - - return user + result := [NUM_GECOS_FIELDS]string{0: "_"} + for i := range result { + result[i] = "_" + } + parts := strings.Split(user.Gecos, ",") + + for i := 0; i < len(parts) && i < NUM_GECOS_FIELDS; i++ { + if strings.TrimSpace(parts[i]) != "" { + result[i] = parts[i] + } + } + + user.GRR = result[1] + user.Resp = result[2] + user.Course = result[3] + user.Expiry = result[4] + // only set with gecos if empty + if user.Status == "_" { + user.Status = result[5] + } + user.Ltype = result[6] + user.Webdir = result[7] + user.Nobackup = result[8] + + return user } func getGroups() (map[string]string, error) { - groupMap := make(map[string]string) - - l, err := connLDAP() - if err != nil { - return groupMap, err - } - defer l.Close() - - // build search - searchRequest := ldap.NewSearchRequest( - "dc=c3local", - ldap.ScopeWholeSubtree, - ldap.NeverDerefAliases, - 0, 0, false, - "(&(objectClass=posixGroup))", - []string{"gidNumber", "cn"}, - nil, - ) - - // search - sr, err := l.Search(searchRequest) - if err != nil { - return nil, fmt.Errorf("LDAP search failed: %w", err) - } - - // arrange result into a map string->string - for _, entry := range sr.Entries { - gid := entry.GetAttributeValue("gidNumber") - cn := entry.GetAttributeValue("cn") - if gid != "" && cn != "" { - groupMap[gid] = cn - } - } - - return groupMap, nil + groupMap := make(map[string]string) + + l, err := connLDAP() + if err != nil { + return groupMap, err + } + defer l.Close() + + // build search + searchRequest := ldap.NewSearchRequest( + "dc=c3local", + ldap.ScopeWholeSubtree, + ldap.NeverDerefAliases, + 0, 0, false, + "(&(objectClass=posixGroup))", + []string{"gidNumber", "cn"}, + nil, + ) + + // search + sr, err := l.Search(searchRequest) + if err != nil { + return nil, fmt.Errorf("LDAP search failed: %w", err) + } + + // arrange result into a map string->string + for _, entry := range sr.Entries { + gid := entry.GetAttributeValue("gidNumber") + cn := entry.GetAttributeValue("cn") + if gid != "" && cn != "" { + groupMap[gid] = cn + } + } + + return groupMap, nil } func getUIDs() ([]int, error) { - var uidNumbers []int - - l, err := connLDAP() - if err != nil { - return uidNumbers, err - } - defer l.Close() - - // build search - searchRequest := ldap.NewSearchRequest( - "dc=c3local", - ldap.ScopeWholeSubtree, - ldap.NeverDerefAliases, - 0, 0, false, - "(&(objectClass=posixAccount))", - []string{"uidNumber"}, - nil, - ) - - // search - sr, err := l.Search(searchRequest) - if err != nil { - return nil, fmt.Errorf("LDAP search failed: %w", err) - } - - // arrange result into a array of integers - for _, entry := range sr.Entries { - uidStr := entry.GetAttributeValue("uidNumber") - if uidStr != "" { - uid, err := strconv.Atoi(uidStr) - if err != nil { - return nil, fmt.Errorf("invalid UID number: %s", uidStr) - } - uidNumbers = append(uidNumbers, uid) - } - } - - // sort for ease of use :) - sort.Ints(uidNumbers) - - return uidNumbers, nil + var uidNumbers []int + + l, err := connLDAP() + if err != nil { + return uidNumbers, err + } + defer l.Close() + + // build search + searchRequest := ldap.NewSearchRequest( + "dc=c3local", + ldap.ScopeWholeSubtree, + ldap.NeverDerefAliases, + 0, 0, false, + "(&(objectClass=posixAccount))", + []string{"uidNumber"}, + nil, + ) + + // search + sr, err := l.Search(searchRequest) + if err != nil { + return nil, fmt.Errorf("LDAP search failed: %w", err) + } + + // arrange result into a array of integers + for _, entry := range sr.Entries { + uidStr := entry.GetAttributeValue("uidNumber") + if uidStr != "" { + uid, err := strconv.Atoi(uidStr) + if err != nil { + return nil, fmt.Errorf("invalid UID number: %s", uidStr) + } + uidNumbers = append(uidNumbers, uid) + } + } + + // sort for ease of use :) + sort.Ints(uidNumbers) + + return uidNumbers, nil } func loginExists(users []model.User, login string) bool { - return exists(users, func(u model.User) bool { return u.UID == login }) + return exists(users, func(u model.User) bool { return u.UID == login }) } // accepts GRRs in the format "GRRXXXXXXXX" aswell as 8 digit number func grrExists(users []model.User, grr string) bool { - return exists(users, func(u model.User) bool { return u.GRR == grr }) || - exists(users, func(u model.User) bool { return u.GRR == "GRR" + grr }) + return exists(users, func(u model.User) bool { return u.GRR == grr }) || + exists(users, func(u model.User) bool { return u.GRR == "GRR"+grr }) } // generic function for check existence of data types func exists[T any](slice []T, predicate func(T) bool) bool { - for _, item := range slice { - if predicate(item) { - return true - } - } - return false + for _, item := range slice { + if predicate(item) { + return true + } + } + return false } diff --git a/model/opts.go b/model/opts.go index 9f56116e54615d217e54da60607bef3b63bedad9..6f2829f7354c3ca8a36b584dccaceb6d2564edf6 100644 --- a/model/opts.go +++ b/model/opts.go @@ -7,27 +7,28 @@ import ( ) type Opts struct { - GRR string - GID string - UID string - Resp string - Name string - Nobkp string - Ltype string - Shell string - Passwd string - Webdir string - Status string - Course string - Expiry string - Homedir string - Block bool - Unblock bool - Confirm bool + GRR string + GID string + UID string + Resp string + Name string + Nobkp string + Ltype string + Shell string + Passwd string + Webdir string + Status string + Course string + Expiry string + Homedir string + Ignore bool + Block bool + Unblock bool + Confirm bool } func (o *Opts) ToString() string { - return fmt.Sprintf(`Opts: + return fmt.Sprintf(`Opts: GRR %s GID %s UID %s @@ -42,97 +43,136 @@ func (o *Opts) ToString() string { Course %s Expiry %s Homedir %s + Ignore %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.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) } - func (o *Opts) RetrieveOpts(cmd *cobra.Command) error { - var err error - - o.GRR, err = getFlagString(cmd, "grr") - if err != nil { return err } - - o.Resp, err = getFlagString(cmd, "resp") - if err != nil { return err } - - o.Name, err = getFlagString(cmd, "name") - if err != nil { return err } - - o.GID, err = getFlagString(cmd, "group") - if err != nil { return err } - - o.UID, err = getFlagString(cmd, "login") - if err != nil { return err } - - o.Ltype, err = getFlagString(cmd, "type") - if err != nil { return err } - - o.Shell, err = getFlagString(cmd, "shell") - if err != nil { return err } - - o.Webdir, err = getFlagString(cmd, "path") - if err != nil { return err } - - o.Nobkp, err = getFlagString(cmd, "nobkp") - if err != nil { return err } - - o.Status, err = getFlagString(cmd, "status") - if err != nil { return err } - - o.Course, err = getFlagString(cmd, "course") - if err != nil { return err } - - o.Expiry, err = getFlagString(cmd, "expiry") - if err != nil { return err } - - o.Passwd, err = getFlagString(cmd, "passwd") - if err != nil { return err } - - o.Homedir, err = getFlagString(cmd, "homedir") - if err != nil { return err } - - o.Confirm, err = getFlagBool(cmd, "confirm") - if err != nil { return err } - - o.Unblock, err = getFlagBool(cmd, "unblock") - if err != nil { return err } - - o.Block, err = getFlagBool(cmd, "block") - if err != nil { return err } - - return nil + var err error + + o.GRR, err = getFlagString(cmd, "grr") + if err != nil { + return err + } + + o.Resp, err = getFlagString(cmd, "resp") + if err != nil { + return err + } + + o.Name, err = getFlagString(cmd, "name") + if err != nil { + return err + } + + o.GID, err = getFlagString(cmd, "group") + if err != nil { + return err + } + + o.UID, err = getFlagString(cmd, "login") + if err != nil { + return err + } + + o.Ltype, err = getFlagString(cmd, "type") + if err != nil { + return err + } + + o.Shell, err = getFlagString(cmd, "shell") + if err != nil { + return err + } + + o.Webdir, err = getFlagString(cmd, "path") + if err != nil { + return err + } + + o.Nobkp, err = getFlagString(cmd, "nobkp") + if err != nil { + return err + } + + o.Status, err = getFlagString(cmd, "status") + if err != nil { + return err + } + + o.Course, err = getFlagString(cmd, "course") + if err != nil { + return err + } + + o.Expiry, err = getFlagString(cmd, "expiry") + if err != nil { + return err + } + + o.Passwd, err = getFlagString(cmd, "passwd") + if err != nil { + return err + } + + o.Homedir, err = getFlagString(cmd, "homedir") + if err != nil { + return err + } + + o.Confirm, err = getFlagBool(cmd, "confirm") + if err != nil { + return err + } + + o.Unblock, err = getFlagBool(cmd, "unblock") + if err != nil { + return err + } + + o.Block, err = getFlagBool(cmd, "block") + if err != nil { + return err + } + + o.Ignore, err = getFlagBool(cmd, "ignore") + if err != nil { + return err + } + + return nil } -// since this model is used in most subcommands, some -// flags may exist in one command but not in another, -// so if it doesn't exist just set as empty before +// since this model is used in most subcommands, some +// flags may exist in one command but not in another, +// so if it doesn't exist just set as empty before // the error occurs, it won't be used anyway... :D func getFlagString(cmd *cobra.Command, flag string) (string, error) { - if cmd.Flags().Lookup(flag) == nil { - return "", nil - } - - flagValue, err := cmd.Flags().GetString(flag) - if err != nil { - return "", fmt.Errorf("failed to get flag %q: %w", flag, err) - } - return flagValue, nil + if cmd.Flags().Lookup(flag) == nil { + return "", nil + } + + flagValue, err := cmd.Flags().GetString(flag) + if err != nil { + return "", fmt.Errorf("failed to get flag %q: %w", flag, err) + } + return flagValue, nil } func getFlagBool(cmd *cobra.Command, flag string) (bool, error) { - if cmd.Flags().Lookup(flag) == nil { - return false, nil - } - - flagValue, err := cmd.Flags().GetBool(flag) - if err != nil { - return false, fmt.Errorf("failed to get flag %q: %w", flag, err) - } - return flagValue, nil + if cmd.Flags().Lookup(flag) == nil { + return false, nil + } + + flagValue, err := cmd.Flags().GetBool(flag) + if err != nil { + return false, fmt.Errorf("failed to get flag %q: %w", flag, err) + } + return flagValue, nil } diff --git a/model/user.go b/model/user.go index 2456d6e6a88ae3c03e211ed714130fdeb1477120..1bce7ae6f2783f58d95b3f94c2fa1b93ac77152e 100644 --- a/model/user.go +++ b/model/user.go @@ -3,24 +3,24 @@ package model import "fmt" type User struct { - DN string - GRR string - UID string - GID string - Name string - Resp string - Ltype string - Gecos string - Shell string - Course string - Status string - Webdir string - Expiry string - Homedir string - Password string - Nobackup string - GIDNumber string - UIDNumber string + DN string + GRR string + UID string + GID string + Name string + Resp string + Ltype string + Gecos string + Shell string + Course string + Status string + Webdir string + Expiry string + Homedir string + Password string + Nobackup string + GIDNumber string + UIDNumber string } func (u *User) ToString() string { @@ -36,14 +36,14 @@ func (u *User) ToString() string { Status: %s Resp: %s Course: %s - Expiry: %s`, - u.Name, u.UID, u.GRR, u.GID, u.Shell, u.Homedir, u.Webdir, + Expiry: %s`, + u.Name, u.UID, u.GRR, u.GID, u.Shell, u.Homedir, u.Webdir, u.Nobackup, u.Status, u.Resp, u.Course, u.Expiry) } // useful for debugging :) func (u *User) FullToString() string { - return fmt.Sprintf(`User: + return fmt.Sprintf(`User: DN: %s GRR: %s UID: %s @@ -62,7 +62,7 @@ func (u *User) FullToString() string { Nobackup: %s GIDNumber: %s UIDNumber: %s`, - u.DN, u.GRR, u.UID, u.GID, u.Name, u.Resp, u.Ltype, u.Gecos, - u.Shell,u.Course, u.Status, u.Webdir, u.Expiry, u.Homedir, + u.DN, u.GRR, u.UID, u.GID, u.Name, u.Resp, u.Ltype, u.Gecos, + u.Shell, u.Course, u.Status, u.Webdir, u.Expiry, u.Homedir, u.Password, u.Nobackup, u.GIDNumber, u.UIDNumber) }