From f003565e83e0eee6ba91b986f5a0ed4d082a2737 Mon Sep 17 00:00:00 2001 From: tss24 <tss24@inf.ufpr.br> Date: Fri, 28 Feb 2025 21:43:38 -0300 Subject: [PATCH] Holy Refactor: OOP --- cmd/user.go | 5 +- cmd/user/bulk.go | 154 ++----------- cmd/user/create.go | 434 ++++--------------------------------- cmd/user/create_test.go | 110 ---------- cmd/user/mod.go | 83 +++---- cmd/user/remove.go | 166 ++------------ cmd/user/reset.go | 55 ++--- cmd/user/show.go | 317 ++------------------------- cmd/user/temp.go | 197 +++++++++++++++++ cmd/user/validation.go | 81 +------ extras/create_test.go | 112 ++++++++++ model/krb.go | 46 ++++ model/ldap.go | 281 ++++++++++++++++++++++++ model/ltype.go | 40 ++++ model/opts.go | 41 ++-- model/user.go | 466 +++++++++++++++++++++++++++++++++++++--- utils/utils.go | 197 +++++++++++++++++ 17 files changed, 1486 insertions(+), 1299 deletions(-) delete mode 100644 cmd/user/create_test.go create mode 100644 cmd/user/temp.go create mode 100644 extras/create_test.go create mode 100644 model/krb.go create mode 100644 model/ldap.go create mode 100644 model/ltype.go create mode 100644 utils/utils.go diff --git a/cmd/user.go b/cmd/user.go index 21cfa72..c2347b0 100644 --- a/cmd/user.go +++ b/cmd/user.go @@ -8,14 +8,13 @@ import ( var userCmd = &cobra.Command{ Use: "user", Short: "User subcommand", - 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.`, + Long: `Subcommand for managing users in general.`, } func init() { userCmd.AddCommand(user.ModCmd) userCmd.AddCommand(user.ShowCmd) + userCmd.AddCommand(user.TempCmd) userCmd.AddCommand(user.BulkCmd) userCmd.AddCommand(user.ResetCmd) userCmd.AddCommand(user.CreateCmd) diff --git a/cmd/user/bulk.go b/cmd/user/bulk.go index 9b45291..ed92d59 100644 --- a/cmd/user/bulk.go +++ b/cmd/user/bulk.go @@ -1,178 +1,68 @@ package user -// TODO: PQ Q N FUNCIONA?????????????? import ( "fmt" - "os" - "strconv" "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 similar users", - RunE: bulkCreate, + 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("passwd", "p", "", "Base password, will generate <password>#[1..number]") - BulkCmd.Flags().StringP("login", "l", "", "Base login, will generate <login>[1..number]") - BulkCmd.Flags().StringP("expiry", "e", "_", "Accounts' expiry date (format dd.mm.yy)") - BulkCmd.Flags().StringP("resp", "r", "_", "Person responsible for the accounts") BulkCmd.Flags().StringP("group", "g", "", "Base group of the accounts") - BulkCmd.Flags().IntP("number", "n", 0, "Number of accounts to be created") + BulkCmd.Flags().StringP("infile", "i", "", "File path with all users") - BulkCmd.MarkFlagRequired("login") - BulkCmd.MarkFlagRequired("passwd") BulkCmd.MarkFlagRequired("group") - BulkCmd.MarkFlagRequired("number") + BulkCmd.MarkFlagRequired("infile") } func bulkCreate(cmd *cobra.Command, args []string) error { var opts model.Opts - var users []model.User + var accounts []model.User - users, err := getUsers() + l, err := model.ConnLDAP() if err != nil { return err } + defer l.Close() err = opts.RetrieveOpts(cmd) if err != nil { return err } - for i := 1; i <= opts.Number; i++ { - if loginExists(users, opts.UID+strconv.Itoa(i)) { - return fmt.Errorf("User found with login %v%v, won't overwrite", opts.UID, i) - } - } - - err = validateGID(opts.GID) - if err != nil { - return err - } - - base := model.User{ - GRR: "_", - UID: opts.UID, - GID: opts.GID, - Name: "_", - Resp: opts.Resp, - Ltype: "_", - Shell: "/bin/bash", - Status: "Active", - Expiry: opts.Expiry, - Webdir: "_", - Password: opts.Passwd + "#", + if !utils.PathExists(opts.Path) { + return fmt.Errorf("Path: \"%v\": no such file", opts.Path) } - base.Homedir, err = genDirPath("/home", base.GID, base.UID, "") + accounts, err = extractModelsFile(opts.Path) if err != nil { return err } - base.Nobackup, err = genDirPath("/nobackup", base.GID, base.UID, "") - if err != nil { - return err - } - - i := 1 - success := false - - defer func() { - if !success { - fmt.Println("Error found, cleaning up...") - for ; i > 0; i-- { - istring := strconv.Itoa(i) - _ = delKerberosPrincipal(base.UID + istring) - _ = delUserLDAP(base.UID + istring) - _ = os.RemoveAll(base.Nobackup + istring) - _ = os.RemoveAll(base.Homedir + istring) - } - } - }() - - for ; i <= opts.Number; i++ { - err := createTempUser(base, i) + for i := range accounts { + err = accounts[i].Create(l) if err != nil { return err } } - success = true return nil } -func createTempUser(base model.User, num int) error { - var err error - var groups map[string]string - numstring := strconv.Itoa(num) - - groups, err = getGroups() - if err != nil { - return err - } - - // gen login - base.UID = base.UID + numstring - - // gen dn - base.DN = "uid=" + base.UID + ",ou=usuarios,dc=c3local" - - // no webdir for temps - - // gen home path - base.Homedir = base.Homedir + numstring - - // gen nobkp path - base.Nobackup = base.Nobackup + numstring - - // gen password - base.Password = base.Password + numstring - - // gen gecos - base.Gecos = genGecos(base) - - // get group id - base.GIDNumber, err = findGIDNumber(groups, base.GID) - if err != nil { - return err - } - - // gen newuid - base.UIDNumber, err = getNewUIDNumber() - if err != nil { - return err - } - - fmt.Printf("Pronto para criar:\n%v\n\n", base.FullToString()) - confirmationPrompt(false, "creation") - - // create ldap - err = addUserLDAP(base) - if err != nil { - return err - } - // create kerberos - err = addKerberosPrincipal(base.UID) - if err != nil { - return err - } - - // change pass - err = modKerberosPassword(base.UID, base.Password) - if err != nil { - return err - } - - // create dirs - err = createUserDirs(base) - if err != nil { - return err - } - - return nil +func extractModelsFile(path string) ([]model.User, error) { + // TODO + return nil, nil } diff --git a/cmd/user/create.go b/cmd/user/create.go index 7b4c9b5..6f4dbef 100644 --- a/cmd/user/create.go +++ b/cmd/user/create.go @@ -2,15 +2,11 @@ package user import ( "fmt" - "os" - "os/exec" - "path/filepath" - "strconv" - "strings" "github.com/go-ldap/ldap/v3" "github.com/spf13/cobra" "gitlab.c3sl.ufpr.br/tss24/useradm/model" + "gitlab.c3sl.ufpr.br/tss24/useradm/utils" ) const ( @@ -34,7 +30,7 @@ func init() { // FIXME: maybe leave less flags for user input CreateCmd.Flags().StringP("grr", "r", "_", "User GRR, required for ini type") CreateCmd.Flags().StringP("type", "t", "ini", "Type of auto-generated login: ini, first or last") - CreateCmd.Flags().StringP("path", "w", "", "Full path to webdir, /home/html/inf/login if empty") + CreateCmd.Flags().StringP("web", "w", "", "Full path to webdir, /home/html/inf/login if empty") CreateCmd.Flags().StringP("name", "n", "_", "User full name, required, use quotes for spaces") CreateCmd.Flags().StringP("login", "l", "", "User login name, auto-generated if empty") CreateCmd.Flags().StringP("group", "g", "", "User base group, required") @@ -54,45 +50,28 @@ func init() { } func createUserFunc(cmd *cobra.Command, args []string) error { - success := false - // creates model from users input - usr, confirm, err := createNewUserModel(cmd) + l, err := model.ConnLDAP() if err != nil { return err } + defer l.Close() - defer func() { - if !success { - _ = delKerberosPrincipal(usr.UID) - _ = delUserLDAP(usr.UID) - } - }() + // creates model from users input + usr, confirm, err := createNewUserModel(cmd, l) + if err != nil { + return err + } // 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") + utils.ConfirmationPrompt(confirm, "creation") if usr.Password == "[auto-generate]" { - usr.Password = genPassword() - } - - err = addUserLDAP(usr) - if err != nil { - return err - } - - err = addKerberosPrincipal(usr.UID) - if err != nil { - return err + usr.Password = utils.GenPassword() } - err = modKerberosPassword(usr.UID, usr.Password) - if err != nil { - return err - } - - err = createUserDirs(usr) + err = usr.Create(l) if err != nil { return err } @@ -103,20 +82,20 @@ func createUserFunc(cmd *cobra.Command, args []string) error { 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 return nil } // creates and validates user inputs into the User model -func createNewUserModel(cmd *cobra.Command) (model.User, bool, error) { +func createNewUserModel(cmd *cobra.Command, l *ldap.Conn) (model.User, bool, error) { var u model.User var opts model.Opts - users, err := getUsers() + + users, err := model.GetAllUsersLDAP(l) if err != nil { return u, false, err } - groups, err := getGroups() + groups, err := model.GetAllGroupsLDAP(l) if err != nil { return u, false, err } @@ -138,24 +117,17 @@ func createNewUserModel(cmd *cobra.Command) (model.User, bool, error) { 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) + opts.Homedir, err = utils.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) + opts.Nobkp, err = utils.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) + opts.Webdir, err = utils.GenDirPath("/home/html/inf", "", opts.UID, opts.Webdir) if err != nil { return u, false, err } @@ -166,389 +138,66 @@ func createNewUserModel(cmd *cobra.Command) (model.User, bool, error) { GRR: opts.GRR, Resp: opts.Resp, Name: opts.Name, - Ltype: opts.Ltype, Shell: opts.Shell, Status: opts.Status, Expiry: opts.Expiry, Webdir: opts.Webdir, Homedir: opts.Homedir, Nobackup: opts.Nobkp, - Password: ifThenElse(opts.Passwd != "", opts.Passwd, "[auto-generate]"), + Password: utils.IfThenElse(opts.Passwd != "", opts.Passwd, "[auto-generate]"), } - u.Gecos = genGecos(u) + u.Ltype.Parse(opts.Ltype) + + if opts.UID == "" { + err = u.GenUniqueUID(users) + if err != nil { + return u, false, err + } + } // get a new UIDNumber - u.UIDNumber, err = getNewUIDNumber() + err = u.GetNewUIDNumber(l) if err != nil { return u, false, fmt.Errorf("failed to generate new UIDNumber for user: %v", err) } // assign GIDNumber by traversing the groups - u.GIDNumber, err = findGIDNumber(groups, u.GID) + u.GIDNumber, err = utils.GetGIDNumFromGID(groups, u.GID) if err != nil { return u, false, err } - u.DN = "uid=" + u.UID + ",ou=usuarios,dc=c3local" + u.GenGecos() + u.SetDN(u.UID) 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) -} - -func genGecos(u model.User) string { - gecos := u.Name + "," - gecos += u.GRR + "," - gecos += u.Resp + "," - gecos += u.Expiry + "," - gecos += u.Status + "," - gecos += u.Ltype + "," - gecos += u.Webdir + "," - gecos += u.Nobackup - return gecos -} - -type LoginType int - -const ( - Initials LoginType = iota - FirstName - LastName -) - -func genLogin(name string, grr string, ltype LoginType, variance int) string { - parts := formatName(name) - if len(parts) == 0 { - return "_" - } - - partPrefixLen := make([]int, len(parts)) - if ltype == Initials { - // the first letter of each part must appear - for i := 0; i < len(parts); i++ { - partPrefixLen[i] = 1 - } - } else if ltype == FirstName { - // the first name(part) must appear - partPrefixLen[0] = len(parts[0]) - } else { - // the first letter of each part must appear - for i := 0; i < len(parts); i++ { - partPrefixLen[i] = 1 - } - // just as the last part - partPrefixLen[len(parts)-1] = len(parts[len(parts)-1]) - } - - partPrefixIx := 0 - for i := 0; i < variance; i++ { - ok := false - for k := 0; k < len(parts) && !ok; k++ { - if partPrefixLen[partPrefixIx] < len(parts[partPrefixIx]) { - partPrefixLen[partPrefixIx]++ - ok = true - } - partPrefixIx = (partPrefixIx + 1) % len(parts) - } - if !ok { - // it's joever, from now on nothing happens, quit :D - break - } - } - - // contruct the login with the given legths - login := "" - for i := 0; i < len(parts); i++ { - login += parts[i][:partPrefixLen[i]] - } - if login == "" { - return "_" - } - - if ltype == Initials { - login += grr[2:4] - } - return login -} - -// 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} - splitName := strings.Fields(name) - var parts []string - - for _, part := range splitName { - lowerPart := strings.ToLower(part) - if !connectives[lowerPart] { - parts = append(parts, string(lowerPart)) - } - } - return parts -} - -func genUniqueUID(name, grr string, ltypeString string, users []model.User) (string, error) { - var uid string - used, variance := true, 0 - for used { - var ltype LoginType - if ltypeString == "ini" { - ltype = Initials - } else if ltypeString == "first" { - ltype = FirstName - } else { - ltype = LastName - } - uid = genLogin(name, grr, ltype, variance) - - // 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 -} - -func findGIDNumber(groups map[string]string, GID string) (string, error) { - for key, val := range groups { - if val == GID { - return key, nil - } - } - return "", fmt.Errorf("Couldn't find group GIDNumber") -} - -// 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 -} - -// 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 -} +func validateInputs(opts model.Opts) error { -// generates a LDAP request and adds the user to LDAP -func addUserLDAP(u model.User) error { - l, err := connLDAP() + l, err := model.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("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 = addUserGroupLDAP(l, u.UID, u.GID) + users, err := model.GetAllUsersLDAP(l) if err != nil { return err } - return nil -} - -// adds a user to a group in LDAP -func addUserGroupLDAP(l *ldap.Conn, UID, GID string) error { - groupDN := fmt.Sprintf("cn=%s,ou=grupos,dc=c3local", GID) - modReq := ldap.NewModifyRequest(groupDN, nil) - modReq.Add("memberUid", []string{UID}) - - err := l.Modify(modReq) - if err != nil { - return fmt.Errorf("Failed to add user %s to group %s: %v", UID, GID, err) - } - - return nil -} - -// creates a KerberosPrincipal for the user -func addKerberosPrincipal(login string) error { - cmd := exec.Command("kadmin.local", "-q", - fmt.Sprintf("addprinc -policy padrao -randkey -x dn=uid=%s,ou=usuarios,dc=c3local %s", login, login)) - - output, err := cmd.CombinedOutput() + groups, err := model.GetAllGroupsLDAP(l) if err != nil { - return fmt.Errorf("Failed to add Kerberos principal: %v\nOutput: %s", err, output) - } - - return nil -} - -func createUserDirs(u model.User) error { - success := false - - 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.Nobackup); err != nil { return err } - if err := createWeb(u); err != nil { - return err - } - - success = true - return nil -} - -func createHome(u model.User, homeDir string) error { - - perm := DEF_PERMISSION - if u.Status == "Blocked" { - perm = BLK_PERMISSION - } - - // create directory - cmd := exec.Command("mkdir", "-p", homeDir) - cmd.Stdout = nil - cmd.Stderr = nil - if err := cmd.Run(); err != nil { - return fmt.Errorf("Failed to create home directory: %w", err) - } - - // copy /etc/skel - cmd = exec.Command("cp", "-r", "/etc/skel/.", homeDir) - if err := cmd.Run(); err != nil { - return fmt.Errorf("Failed to copy /etc/skel contents: %w", err) - } - - // change permissions - 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) - if err := cmd.Run(); err != nil { - return fmt.Errorf("Failed to change ownership: %w", err) - } - - return nil -} - -func createWeb(u model.User) error { - success := false - - if u.Webdir == "_" { - success = true - return nil - } - - perm := DEF_PERMISSION - if u.Status == "Blocked" { - perm = BLK_PERMISSION - } - - 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 { - return fmt.Errorf("Failed to create web directory: %w", err) - } - - // create index - cmd = exec.Command("touch", filepath.Join(u.Webdir, "index.html")) - if err := cmd.Run(); err != nil { - return fmt.Errorf("Failed to create index.html: %w", err) - } - - // 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) - 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) - if err := cmd.Run(); err != nil { - return fmt.Errorf("Failed to change ownership: %w", err) - } - - // 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 -} - -func validateInputs(opts model.Opts) error { - var err error - - err = validateGID(opts.GID) + err = validateGID(groups, opts.GID) if err != nil { return err } - err = validateGRR(opts.GRR) + err = validateGRR(users, opts.GRR) if err != nil { return err } @@ -563,14 +212,9 @@ func validateInputs(opts model.Opts) error { 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) + err := validateUID(users, opts.UID) if err != nil { return err } diff --git a/cmd/user/create_test.go b/cmd/user/create_test.go deleted file mode 100644 index ae52d43..0000000 --- a/cmd/user/create_test.go +++ /dev/null @@ -1,110 +0,0 @@ -package user - -import ( - "reflect" - "testing" -) - -func assertEqual(t *testing.T, a any, b any) { - if !reflect.DeepEqual(a, b) { - t.Fatalf("%s != %s", a, b) - } -} - -func TestGenLogin(t *testing.T) { - assertEqual(t, genLogin("", "20241982", Initials, 0), "_") - assertEqual(t, genLogin("", "20241982", Initials, 1), "_") - assertEqual(t, genLogin("", "20241982", Initials, 2), "_") - assertEqual(t, genLogin("", "20241982", Initials, 3), "_") - - assertEqual(t, genLogin("", "20241982", FirstName, 0), "_") - assertEqual(t, genLogin("", "20241982", FirstName, 1), "_") - assertEqual(t, genLogin("", "20241982", FirstName, 2), "_") - assertEqual(t, genLogin("", "20241982", FirstName, 3), "_") - - assertEqual(t, genLogin("", "20241982", LastName, 0), "_") - assertEqual(t, genLogin("", "20241982", LastName, 1), "_") - assertEqual(t, genLogin("", "20241982", LastName, 2), "_") - assertEqual(t, genLogin("", "20241982", LastName, 3), "_") - - assertEqual(t, genLogin("de", "20241982", Initials, 0), "_") - assertEqual(t, genLogin("de", "20241982", Initials, 1), "_") - assertEqual(t, genLogin("de", "20241982", Initials, 2), "_") - assertEqual(t, genLogin("de", "20241982", Initials, 3), "_") - - assertEqual(t, genLogin("de", "20241982", FirstName, 0), "_") - assertEqual(t, genLogin("de", "20241982", FirstName, 1), "_") - assertEqual(t, genLogin("de", "20241982", FirstName, 2), "_") - assertEqual(t, genLogin("de", "20241982", FirstName, 3), "_") - - assertEqual(t, genLogin("de", "20241982", LastName, 0), "_") - assertEqual(t, genLogin("de", "20241982", LastName, 1), "_") - assertEqual(t, genLogin("de", "20241982", LastName, 2), "_") - assertEqual(t, genLogin("de", "20241982", LastName, 3), "_") - - assertEqual(t, genLogin("da de", "20241982", Initials, 0), "_") - assertEqual(t, genLogin("da de", "20241982", Initials, 1), "_") - assertEqual(t, genLogin("da de", "20241982", Initials, 2), "_") - assertEqual(t, genLogin("da de", "20241982", Initials, 3), "_") - - assertEqual(t, genLogin("da de", "20241982", FirstName, 0), "_") - assertEqual(t, genLogin("da de", "20241982", FirstName, 1), "_") - assertEqual(t, genLogin("da de", "20241982", FirstName, 2), "_") - assertEqual(t, genLogin("da de", "20241982", FirstName, 3), "_") - - assertEqual(t, genLogin("da de", "20241982", LastName, 0), "_") - assertEqual(t, genLogin("da de", "20241982", LastName, 1), "_") - assertEqual(t, genLogin("da de", "20241982", LastName, 2), "_") - assertEqual(t, genLogin("da de", "20241982", LastName, 3), "_") - - assertEqual(t, genLogin("Fabiano", "20241982", Initials, 0), "f24") - assertEqual(t, genLogin("Fabiano", "20241982", Initials, 1), "fa24") - assertEqual(t, genLogin("Fabiano", "20241982", Initials, 2), "fab24") - assertEqual(t, genLogin("Fabiano", "20241982", Initials, 3), "fabi24") - - assertEqual(t, genLogin("Fabiano", "20241982", FirstName, 0), "fabiano") - assertEqual(t, genLogin("Fabiano", "20241982", FirstName, 1), "fabiano") - assertEqual(t, genLogin("Fabiano", "20241982", FirstName, 2), "fabiano") - assertEqual(t, genLogin("Fabiano", "20241982", FirstName, 3), "fabiano") - - assertEqual(t, genLogin("Fabiano", "20241982", LastName, 0), "fabiano") - assertEqual(t, genLogin("Fabiano", "20241982", LastName, 1), "fabiano") - assertEqual(t, genLogin("Fabiano", "20241982", LastName, 2), "fabiano") - assertEqual(t, genLogin("Fabiano", "20241982", LastName, 3), "fabiano") - - assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", Initials, 0), "faps24") - assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", Initials, 1), "faaps24") - assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", Initials, 2), "faanps24") - assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", Initials, 3), "faanpes24") - assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", Initials, 14), "fabiaantunperesouz24") - assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", Initials, 18), "fabianantunepereisouza24") - assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", Initials, 19), "fabianantunepereirsouza24") - assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", Initials, 20), "fabianoantunepereirsouza24") - assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", Initials, 21), "fabianoantunespereirsouza24") - assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", Initials, 22), "fabianoantunespereirasouza24") - assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", Initials, 40), "fabianoantunespereirasouza24") - assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", Initials, 50), "fabianoantunespereirasouza24") - - assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", FirstName, 0), "fabiano") - assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", FirstName, 1), "fabianoa") - assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", FirstName, 2), "fabianoap") - assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", FirstName, 3), "fabianoaps") - assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", FirstName, 4), "fabianoanps") - assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", FirstName, 16), "fabianoantunepereisouza") - assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", FirstName, 17), "fabianoantunepereirsouza") - assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", FirstName, 18), "fabianoantunespereirsouza") - assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", FirstName, 19), "fabianoantunespereirasouza") - assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", FirstName, 20), "fabianoantunespereirasouza") - assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", FirstName, 50), "fabianoantunespereirasouza") - - assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", LastName, 0), "fapsouza") - assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", LastName, 1), "faapsouza") - assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", LastName, 2), "faanpsouza") - assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", LastName, 3), "faanpesouza") - assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", LastName, 15), "fabianantunepereirsouza") - assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", LastName, 16), "fabianoantunepereirsouza") - assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", LastName, 17), "fabianoantunespereirsouza") - assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", LastName, 18), "fabianoantunespereirasouza") - assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", LastName, 19), "fabianoantunespereirasouza") - assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", LastName, 100), "fabianoantunespereirasouza") -} diff --git a/cmd/user/mod.go b/cmd/user/mod.go index 2c06d20..fbb397f 100644 --- a/cmd/user/mod.go +++ b/cmd/user/mod.go @@ -1,15 +1,14 @@ package user import ( - "bufio" "fmt" "os" "os/exec" - "strings" "github.com/go-ldap/ldap/v3" "github.com/spf13/cobra" "gitlab.c3sl.ufpr.br/tss24/useradm/model" + "gitlab.c3sl.ufpr.br/tss24/useradm/utils" "gopkg.in/yaml.v3" ) @@ -38,12 +37,28 @@ func init() { func modifyUserFunc(cmd *cobra.Command, args []string) error { var opts model.Opts - err := opts.RetrieveOpts(cmd) + l, err := model.ConnLDAP() + if err != nil { + return err + } + defer l.Close() + + users, err := model.GetAllUsersLDAP(l) + if err != nil { + return err + } + + groups, err := model.GetAllGroupsLDAP(l) if err != nil { return err } - curr, err := locateUser(args[0]) + err = opts.RetrieveOpts(cmd) + if err != nil { + return err + } + + curr, err := model.Locate(l, args[0]) if err != nil { return err } @@ -61,13 +76,13 @@ func modifyUserFunc(cmd *cobra.Command, args []string) error { changes, err := promptUserYaml(state) if changes.GRR != curr.GRR { - err = validateGRR(changes.GRR) + err = validateGRR(users, changes.GRR) if err != nil { return err } } - err = validateGID(changes.Group) + err = validateGID(groups, changes.Group) if err != nil { return err } @@ -84,35 +99,29 @@ func modifyUserFunc(cmd *cobra.Command, args []string) error { oldGroup := curr.GID - req, err := genRequest(curr, changes) + req, err := genRequest(groups, curr, changes) if err != nil { return err } fmt.Printf("%v\n\n", curr.ToString()) - confirmationPrompt(opts.Confirm, "update") - - l, err := connLDAP() - if err != nil { - return err - } - defer l.Close() + utils.ConfirmationPrompt(opts.Confirm, "update") if err := l.Modify(req); err != nil { return fmt.Errorf("Failed to update user attributes: %v", err) } if oldGroup != changes.Group { - if err := delUserFromGroupLDAP(l, curr.UID, curr.GID); err != nil { + if err := model.DelFromGroupLDAP(l, curr.UID, curr.GID); err != nil { return err } - if err := addUserGroupLDAP(l, curr.UID, changes.Group); err != nil { + if err := model.AddToGroupLDAP(l, curr.UID, changes.Group); err != nil { return err } } - if err := clearCache(); err != nil { + if err := utils.ClearCache(); err != nil { fmt.Printf(`Failed to reload cache! all is ok but may take a while to apply the changes Output: %v`, err) @@ -123,7 +132,9 @@ Output: %v`, err) return nil } -func genRequest(curr model.User, changes cfg) (*ldap.ModifyRequest, error) { +func genRequest(groups map[string]string, + curr model.User, changes cfg) (*ldap.ModifyRequest, error) { + req := ldap.NewModifyRequest(curr.DN, nil) // changed name @@ -149,11 +160,6 @@ func genRequest(curr model.User, changes cfg) (*ldap.ModifyRequest, error) { // changed base group if curr.GID != changes.Group { - groups, err := getGroups() - if err != nil { - return req, err - } - var gidNumber string for id, gid := range groups { if gid == changes.Group { @@ -167,7 +173,7 @@ func genRequest(curr model.User, changes cfg) (*ldap.ModifyRequest, error) { changed := applyChangesToUser(curr, changes) - changed.Gecos = genGecos(changed) + changed.GenGecos() // changed gecos if curr.Gecos != changed.Gecos { @@ -233,32 +239,3 @@ func promptUserYaml(state cfg) (cfg, error) { return newState, nil } - -// 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) - - reader := bufio.NewReader(os.Stdin) - response, _ := reader.ReadString('\n') - - if strings.TrimSpace(strings.ToLower(response)) != "y" { - fmt.Fprintln(os.Stderr, "Aborted.") - os.Exit(1) - } - } -} - -func clearCache() error { - cmd := exec.Command("nscd", "-i", "passwd") - - cmd.Stdout = nil - cmd.Stderr = nil - err := cmd.Run() - if err != nil { - return err - } - - cmd = exec.Command("nscd", "-i", "group") - return cmd.Run() -} diff --git a/cmd/user/remove.go b/cmd/user/remove.go index 0c10753..24fbf44 100644 --- a/cmd/user/remove.go +++ b/cmd/user/remove.go @@ -3,15 +3,13 @@ package user import ( "fmt" "log" - "os" - "os/exec" "path/filepath" "strconv" "time" - "github.com/go-ldap/ldap/v3" "github.com/spf13/cobra" "gitlab.c3sl.ufpr.br/tss24/useradm/model" + "gitlab.c3sl.ufpr.br/tss24/useradm/utils" ) var ( @@ -36,13 +34,19 @@ func removeUserFunc(cmd *cobra.Command, args []string) error { var opts model.Opts success := false - err := opts.RetrieveOpts(cmd) + l, err := model.ConnLDAP() + if err != nil { + return err + } + defer l.Close() + + err = opts.RetrieveOpts(cmd) if err != nil { return err } login := args[0] - u, err := locateUser(login) + u, err := model.Locate(l, login) if err != nil { return err } @@ -50,25 +54,25 @@ func removeUserFunc(cmd *cobra.Command, args []string) error { 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) + _ = utils.MoveAndChown(filepath.Join(NO_BKP_TRASH, + filepath.Base(u.Nobackup)), u.Nobackup, u.UID, u.GID) + _ = utils.MoveAndChown(filepath.Join(HOME_TRASH, + filepath.Base(u.Homedir)), u.Homedir, u.UID, u.GID) + _ = utils.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") + utils.ConfirmationPrompt(opts.Confirm, "removal") - err = removeDirs(u) + err = u.DirsToTrash() if err != nil { return err } - err = delUserLDAP(u.UID) + err = model.DelFromLDAP(u.UID) if err != nil { return err } @@ -78,137 +82,3 @@ func removeUserFunc(cmd *cobra.Command, args []string) error { 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() - - if !loginExists(users, login) { - return u, fmt.Errorf("Failed to find login in LDAP database: %v", err) - } - - filter := searchUser(users, false, true, login, "", "", "", "", "") - if len(filter) != 1 { - return u, fmt.Errorf(`More than one user matched the login given. -search made: "useradm user show -l %v -e"`, login) - } - - 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.Nobackup, NO_BKP_TRASH, "nobody", "nogroup") - if err != nil { - return err - } - - err = moveAndChown(u.Webdir, WEB_TRASH, "nobody", "nogroup") - if err != nil { - return err - } - - return nil -} - -func delUserLDAP(UID string) error { - l, err := connLDAP() - if err != nil { - return err - } - defer l.Close() - - // search for all groups the user is a member of - searchReq := ldap.NewSearchRequest( - "ou=grupos,dc=c3local,dc=com", - ldap.ScopeWholeSubtree, - ldap.NeverDerefAliases, 0, 0, false, - "(memberUid="+UID+")", // Filter by UID membership - []string{"dn", "cn"}, - nil, - ) - - groups, err := l.Search(searchReq) - if err != nil && !ldap.IsErrorWithCode(err, ldap.LDAPResultNoSuchObject) { - return fmt.Errorf("Group members search failed: %v", err) - } - - // iterate and remove user from each group - for _, entry := range groups.Entries { - groupName := entry.GetAttributeValue("cn") - if err := delUserFromGroupLDAP(l, UID, groupName); err != nil { - log.Printf("Warning: %v", err) - } - } - - // removing user entry - userDN := "uid=" + UID + "ou=usuarios,dc=c3local" - delReq := ldap.NewDelRequest(userDN, nil) - if err := l.Del(delReq); err != nil && - !ldap.IsErrorWithCode(err, ldap.LDAPResultNoSuchObject) { - return fmt.Errorf("User deletion failed: %v", err) - } - - return nil -} - -func delUserFromGroupLDAP(l *ldap.Conn, userUID, GID string) error { - groupDN := fmt.Sprintf("cn=%s,ou=grupos,dc=c3local", GID) - modReq := ldap.NewModifyRequest(groupDN, nil) - modReq.Delete("memberUid", []string{userUID}) - - err := l.Modify(modReq) - if err != nil && !ldap.IsErrorWithCode(err, ldap.LDAPResultNoSuchAttribute) { - return fmt.Errorf("Failed to remove user %s from group %s: %v", userUID, groupDN, err) - } - - return nil -} - -// 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)) - - output, err := cmd.CombinedOutput() - if err != nil { - return fmt.Errorf("Fail to delete Kerberos principal: %v\nOutput: %s", err, output) - } - - 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 - } - - // 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) - } - - // move directory - cmd := exec.Command("mv", orig, destPath) - if output, err := cmd.CombinedOutput(); err != nil { - return fmt.Errorf("Failed to move dirs: %w\nOutput: %v", err, output) - } - - // recursive chown - 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/reset.go b/cmd/user/reset.go index 038e918..aea0b26 100644 --- a/cmd/user/reset.go +++ b/cmd/user/reset.go @@ -1,11 +1,11 @@ package user import ( - "crypto/rand" "fmt" - "os/exec" "github.com/spf13/cobra" + "gitlab.c3sl.ufpr.br/tss24/useradm/model" + "gitlab.c3sl.ufpr.br/tss24/useradm/utils" ) var ResetCmd = &cobra.Command{ @@ -25,57 +25,34 @@ func resetPass(cmd *cobra.Command, args []string) error { return err } - users, err := getUsers() + l, err := model.ConnLDAP() if err != nil { return err } - - login := args[0] - res := searchUser(users, false, true, login, "", "", "", "", "") - if len(res) != 1 { - return fmt.Errorf("More than one user found") - } + defer l.Close() if pass == "" { - pass = genPassword() + pass = utils.GenPassword() } - confirmationPrompt(false, "password reset") - err = modKerberosPassword(login, pass) + users, err := model.GetAllUsersLDAP(l) if err != nil { return err } - fmt.Printf("Password for user %v has been reset!\n", login) - fmt.Printf("New Password: %v\n", pass) - 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)] + login := args[0] + res := model.Search(users, false, true, login, "", "", "", "", "") + if len(res) != 1 { + return fmt.Errorf("More than one user found") } - return string(b) -} -// command that changes the password >:D -// the command kadmin.local returns 0 if the password change -// fails, bruh. so we have to check differently. -// FIXME: maybe do the validation with regex? -func modKerberosPassword(login, password string) error { - cmd := exec.Command("kadmin.local", "-q", - fmt.Sprintf("cpw -pw %s %s", password, login)) - - output, _ := cmd.CombinedOutput() - if len(output) > 105 { - return fmt.Errorf("Error found changing password, output: \n%v", string(output[:])) + utils.ConfirmationPrompt(false, "password reset") + err = model.ModKRBPassword(login, pass) + if err != nil { + return err } + fmt.Printf("Password for user %v has been reset!\n", login) + fmt.Printf("New Password: %v\n", pass) return nil } diff --git a/cmd/user/show.go b/cmd/user/show.go index 5e9ad4f..6bb389e 100644 --- a/cmd/user/show.go +++ b/cmd/user/show.go @@ -2,25 +2,15 @@ package user import ( "fmt" - "os" - "sort" - "strconv" - "strings" - "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" -) - var ShowCmd = &cobra.Command{ Use: "show", Short: "Search for users and show info", - RunE: searchUserFunc, + RunE: searchUserCmd, } func init() { @@ -39,312 +29,37 @@ func init() { ShowCmd.Flags().BoolP("exact", "e", false, "Make the matching be exact") } -func searchUserFunc(cmd *cobra.Command, args []string) error { +func searchUserCmd(cmd *cobra.Command, args []string) error { var o model.Opts - users, err := getUsers() - if err != nil { - return err - } - - err = o.RetrieveOpts(cmd) + l, err := model.ConnLDAP() if err != nil { return err } - - filtered := searchUser(users, o.Ignore, o.Exact, o.UID, o.GID, - o.Name, o.GRR, o.Status, o.Homedir) - - if len(filtered) == 0 { - fmt.Printf("No user matched the search!") - return nil - } - - for i := range filtered { - fmt.Printf("%v\n\n", filtered[i].ToString()) - } - - return nil -} - -func searchUser(users []model.User, ig, ex bool, l, g, n, r, s, h string) []model.User { - return Filter(users, func(u model.User) bool { - matches := func(src, target string) bool { - if ig { - src, target = strings.ToLower(src), strings.ToLower(target) - } - if ex { - return src == target // exact match - } - return strings.Contains(src, target) // partial match - } - - // search - return (r == "" || matches(u.GRR, r)) && - (n == "" || matches(u.Name, n)) && - (l == "" || matches(u.UID, l)) && - (g == "" || matches(u.GID, g)) && - (s == "" || matches(u.Status, s)) && - (h == "" || matches(u.Homedir, h)) - }) -} - -// 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 -} - -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() + users, err := model.GetAllUsersLDAP(l) 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 -} - -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 - } - - password := strings.TrimSpace(string(passwd)) - - return password, nil -} - -// 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 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.Expiry = result[3] - // only set with gecos if empty - if user.Status == "_" { - user.Status = result[4] - } - user.Ltype = result[5] - user.Webdir = result[6] - user.Nobackup = result[7] - - 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 err } - return groupMap, nil -} - -func getUIDs() ([]int, error) { - var uidNumbers []int - - l, err := connLDAP() + err = o.RetrieveOpts(cmd) if err != nil { - return uidNumbers, err + return err } - defer l.Close() - // build search - searchRequest := ldap.NewSearchRequest( - "dc=c3local", - ldap.ScopeWholeSubtree, - ldap.NeverDerefAliases, - 0, 0, false, - "(&(objectClass=posixAccount))", - []string{"uidNumber"}, - nil, - ) + found := model.Search(users, o.Ignore, o.Exact, o.UID, + o.GID, o.Name, o.GRR, o.Status, o.Homedir) - // search - sr, err := l.Search(searchRequest) - if err != nil { - return nil, fmt.Errorf("LDAP search failed: %w", err) + if len(found) == 0 { + fmt.Println("No user matched the search!") + return nil } - // 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) - } + for i := range found { + //fmt.Printf("%v\n\n", found[i].ToString()) + fmt.Printf("%v\n\n", found[i].FullToString()) // for debugging } - // 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 }) -} - -// 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 }) -} - -// 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 + return nil } diff --git a/cmd/user/temp.go b/cmd/user/temp.go new file mode 100644 index 0000000..cd76e7b --- /dev/null +++ b/cmd/user/temp.go @@ -0,0 +1,197 @@ +package user + +// TODO: PQ Q N FUNCIONA?????????????? +import ( + "fmt" + "os" + "strconv" + + "github.com/spf13/cobra" + "gitlab.c3sl.ufpr.br/tss24/useradm/model" + "gitlab.c3sl.ufpr.br/tss24/useradm/utils" +) + +var TempCmd = &cobra.Command{ + Use: "temp", + Short: "Create a lot of similar temporary users", + RunE: tempCreate, +} + +func init() { + TempCmd.Flags().StringP("passwd", "p", "", "Base password, will generate <password>#[1..number]") + TempCmd.Flags().StringP("login", "l", "", "Base login, will generate <login>[1..number]") + TempCmd.Flags().StringP("expiry", "e", "_", "Accounts' expiry date (format dd.mm.yy)") + TempCmd.Flags().StringP("resp", "r", "_", "Person responsible for the accounts") + TempCmd.Flags().StringP("group", "g", "", "Base group of the accounts") + TempCmd.Flags().IntP("number", "n", 0, "Number of accounts to be created") + + TempCmd.MarkFlagRequired("login") + TempCmd.MarkFlagRequired("passwd") + TempCmd.MarkFlagRequired("group") + TempCmd.MarkFlagRequired("number") +} + +func tempCreate(cmd *cobra.Command, args []string) error { + var opts model.Opts + + l, err := model.ConnLDAP() + if err != nil { + return err + } + defer l.Close() + + users, err := model.GetAllUsersLDAP(l) + if err != nil { + return err + } + + groups, err := model.GetAllGroupsLDAP(l) + if err != nil { + return err + } + + err = opts.RetrieveOpts(cmd) + if err != nil { + return err + } + + for i := 1; i <= opts.Number; i++ { + if model.LoginExists(users, opts.UID+strconv.Itoa(i)) { + return fmt.Errorf("User found with login %v%v, won't overwrite", opts.UID, i) + } + } + + err = validateGID(groups, opts.GID) + if err != nil { + return err + } + + base := model.User{ + GRR: "_", + UID: opts.UID, + GID: opts.GID, + Name: "_", + Resp: opts.Resp, + Ltype: model.LoginTypeUnknown, + Shell: "/bin/bash", + Status: "Active", + Expiry: opts.Expiry, + Webdir: "_", + Password: opts.Passwd + "#", + } + + base.Homedir, err = utils.GenDirPath("/home", base.GID, base.UID, "") + if err != nil { + return err + } + + base.Nobackup, err = utils.GenDirPath("/nobackup", base.GID, base.UID, "") + if err != nil { + return err + } + + i := 1 + success := false + + defer func() { + if !success { + fmt.Println("Error found, cleaning up...") + for ; i > 0; i-- { + istring := strconv.Itoa(i) + _ = model.DelKRBPrincipal(base.UID + istring) + _ = model.DelFromLDAP(base.UID + istring) + _ = os.RemoveAll(base.Nobackup + istring) + _ = os.RemoveAll(base.Homedir + istring) + } + } + }() + + for ; i <= opts.Number; i++ { + err := createTempUser(base, i) + if err != nil { + return err + } + } + + success = true + return nil +} + +func createTempUser(base model.User, num int) error { + var err error + var groups map[string]string + numstring := strconv.Itoa(num) + + l, err := model.ConnLDAP() + if err != nil { + return err + } + defer l.Close() + + groups, err = model.GetAllGroupsLDAP(l) + if err != nil { + return err + } + + // gen login + base.UID = base.UID + numstring + + // gen dn + base.DN = "uid=" + base.UID + ",ou=usuarios,dc=c3local" + + // no webdir for temps + + // gen home path + base.Homedir = base.Homedir + numstring + + // gen nobkp path + base.Nobackup = base.Nobackup + numstring + + // gen password + base.Password = base.Password + numstring + + // gen gecos + base.GenGecos() + + // get group id + base.GIDNumber, err = utils.GetGIDNumFromGID(groups, base.GID) + if err != nil { + return err + } + + // gen newuid + err = base.GetNewUIDNumber(l) + if err != nil { + return err + } + + fmt.Printf("Pronto para criar:\n%v\n\n", base.FullToString()) + utils.ConfirmationPrompt(false, "creation") + + // TODO: change for base.Create() + // create ldap + err = base.AddToLDAP(l) + if err != nil { + return err + } + + // create kerberos + err = model.CreateKRBPrincipal(base.UID) + if err != nil { + return err + } + + // change pass + err = model.ModKRBPassword(base.UID, base.Password) + if err != nil { + return err + } + + // create dirs + err = base.CreateDirs() + if err != nil { + return err + } + + return nil +} diff --git a/cmd/user/validation.go b/cmd/user/validation.go index 26aaeaa..ec2c952 100644 --- a/cmd/user/validation.go +++ b/cmd/user/validation.go @@ -2,20 +2,12 @@ package user import ( "fmt" - "os" "regexp" - "strconv" "strings" - "time" -) -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) -} + "gitlab.c3sl.ufpr.br/tss24/useradm/model" + "gitlab.c3sl.ufpr.br/tss24/useradm/utils" +) func validateExpiry(expiry string) error { if expiry == "_" { @@ -23,13 +15,14 @@ func validateExpiry(expiry string) error { } parts := strings.Split(expiry, ".") - if !isValidDate(parts) { + if !utils.IsValidDate(parts) { err := fmt.Errorf("Malformed expiry date string, use \"dd.mm.yy\"") return err } return nil } +// TODO: change this check func validateStatus(status string) error { if status != "Blocked" && status != "Active" { err := fmt.Errorf("User status can only be \"Active\" or \"Blocked\"") @@ -38,32 +31,19 @@ func validateStatus(status string) error { return nil } -func validateLtype(ltype string) error { - if ltype != "ini" && ltype != "first" && ltype != "last" { - err := fmt.Errorf("Login type can only be \"ini\", \"first\" or \"last\"") - return err - } - return nil -} - -func validateGRR(grr string) error { +func validateGRR(users []model.User, grr string) error { // OK if empty, only "ini" login type requires it and we check :) if grr == "_" { return nil } - users, err := getUsers() - if err != nil { - return err - } - isValid, _ := regexp.MatchString(`^\d{8}$`, grr) // is 8 digit number if !isValid { err := fmt.Errorf("Malformed GRR string, must be 8 digit number") return err } - if grrExists(users, grr) { + if model.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 @@ -72,14 +52,9 @@ Note: To search for the account use "useradm user show -r %s"`, grr) return nil } -func validateGID(group string) error { +func validateGID(groups map[string]string, group string) error { var err error - groups, err := getGroups() - if err != nil { - return err - } - for _, value := range groups { if value == group { return nil @@ -89,47 +64,11 @@ func validateGID(group string) error { return err } -func validateUID(login string) error { - users, err := getUsers() - if err != nil { - return err - } - res := searchUser(users, false, true, login, "", "", "", "", "") +func validateUID(users []model.User, login string) error { + res := model.Search(users, false, true, login, "", "", "", "", "") if len(res) != 0 { return fmt.Errorf(`The informed Login already exists in LDAP database Note: To search for the account use "useradm user show -l %s"`, login) } return nil } - -func isValidDate(arr []string) bool { - if len(arr) != 3 { - return false - } - - // convert to int - 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 { - return false - } - - // ensure year is two digits - if year < 0 || year > 99 { - return false - } - - // validate the date - fullYear := 2000 + year - t := time.Date(fullYear, time.Month(mth), day, 0, 0, 0, 0, time.UTC) - return t.Day() == day && t.Month() == time.Month(mth) -} - -func ifThenElse(condition bool, a string, b string) string { - if condition { - return a - } - return b -} diff --git a/extras/create_test.go b/extras/create_test.go new file mode 100644 index 0000000..61899cf --- /dev/null +++ b/extras/create_test.go @@ -0,0 +1,112 @@ +package extras + +import ( + "reflect" + "testing" + + "gitlab.c3sl.ufpr.br/tss24/useradm/model" +) + +func assertEqual(t *testing.T, a any, b any) { + if !reflect.DeepEqual(a, b) { + t.Fatalf("%s != %s", a, b) + } +} + +func TestGenLogin(t *testing.T) { + assertEqual(t, genLogin("", "20241982", model.Initials, 0), "_") + assertEqual(t, genLogin("", "20241982", model.Initials, 1), "_") + assertEqual(t, genLogin("", "20241982", model.Initials, 2), "_") + assertEqual(t, genLogin("", "20241982", model.Initials, 3), "_") + + assertEqual(t, genLogin("", "20241982", model.FirstName, 0), "_") + assertEqual(t, genLogin("", "20241982", model.FirstName, 1), "_") + assertEqual(t, genLogin("", "20241982", model.FirstName, 2), "_") + assertEqual(t, genLogin("", "20241982", model.FirstName, 3), "_") + + assertEqual(t, genLogin("", "20241982", model.LastName, 0), "_") + assertEqual(t, genLogin("", "20241982", model.LastName, 1), "_") + assertEqual(t, genLogin("", "20241982", model.LastName, 2), "_") + assertEqual(t, genLogin("", "20241982", model.LastName, 3), "_") + + assertEqual(t, genLogin("de", "20241982", model.Initials, 0), "_") + assertEqual(t, genLogin("de", "20241982", model.Initials, 1), "_") + assertEqual(t, genLogin("de", "20241982", model.Initials, 2), "_") + assertEqual(t, genLogin("de", "20241982", model.Initials, 3), "_") + + assertEqual(t, genLogin("de", "20241982", model.FirstName, 0), "_") + assertEqual(t, genLogin("de", "20241982", model.FirstName, 1), "_") + assertEqual(t, genLogin("de", "20241982", model.FirstName, 2), "_") + assertEqual(t, genLogin("de", "20241982", model.FirstName, 3), "_") + + assertEqual(t, genLogin("de", "20241982", model.LastName, 0), "_") + assertEqual(t, genLogin("de", "20241982", model.LastName, 1), "_") + assertEqual(t, genLogin("de", "20241982", model.LastName, 2), "_") + assertEqual(t, genLogin("de", "20241982", model.LastName, 3), "_") + + assertEqual(t, genLogin("da de", "20241982", model.Initials, 0), "_") + assertEqual(t, genLogin("da de", "20241982", model.Initials, 1), "_") + assertEqual(t, genLogin("da de", "20241982", model.Initials, 2), "_") + assertEqual(t, genLogin("da de", "20241982", model.Initials, 3), "_") + + assertEqual(t, genLogin("da de", "20241982", model.FirstName, 0), "_") + assertEqual(t, genLogin("da de", "20241982", model.FirstName, 1), "_") + assertEqual(t, genLogin("da de", "20241982", model.FirstName, 2), "_") + assertEqual(t, genLogin("da de", "20241982", model.FirstName, 3), "_") + + assertEqual(t, genLogin("da de", "20241982", model.LastName, 0), "_") + assertEqual(t, genLogin("da de", "20241982", model.LastName, 1), "_") + assertEqual(t, genLogin("da de", "20241982", model.LastName, 2), "_") + assertEqual(t, genLogin("da de", "20241982", model.LastName, 3), "_") + + assertEqual(t, genLogin("Fabiano", "20241982", model.Initials, 0), "f24") + assertEqual(t, genLogin("Fabiano", "20241982", model.Initials, 1), "fa24") + assertEqual(t, genLogin("Fabiano", "20241982", model.Initials, 2), "fab24") + assertEqual(t, genLogin("Fabiano", "20241982", model.Initials, 3), "fabi24") + + assertEqual(t, genLogin("Fabiano", "20241982", model.FirstName, 0), "fabiano") + assertEqual(t, genLogin("Fabiano", "20241982", model.FirstName, 1), "fabiano") + assertEqual(t, genLogin("Fabiano", "20241982", model.FirstName, 2), "fabiano") + assertEqual(t, genLogin("Fabiano", "20241982", model.FirstName, 3), "fabiano") + + assertEqual(t, genLogin("Fabiano", "20241982", model.LastName, 0), "fabiano") + assertEqual(t, genLogin("Fabiano", "20241982", model.LastName, 1), "fabiano") + assertEqual(t, genLogin("Fabiano", "20241982", model.LastName, 2), "fabiano") + assertEqual(t, genLogin("Fabiano", "20241982", model.LastName, 3), "fabiano") + + assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", model.Initials, 0), "faps24") + assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", model.Initials, 1), "faaps24") + assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", model.Initials, 2), "faanps24") + assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", model.Initials, 3), "faanpes24") + assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", model.Initials, 14), "fabiaantunperesouz24") + assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", model.Initials, 18), "fabianantunepereisouza24") + assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", model.Initials, 19), "fabianantunepereirsouza24") + assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", model.Initials, 20), "fabianoantunepereirsouza24") + assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", model.Initials, 21), "fabianoantunespereirsouza24") + assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", model.Initials, 22), "fabianoantunespereirasouza24") + assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", model.Initials, 40), "fabianoantunespereirasouza24") + assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", model.Initials, 50), "fabianoantunespereirasouza24") + + assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", model.FirstName, 0), "fabiano") + assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", model.FirstName, 1), "fabianoa") + assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", model.FirstName, 2), "fabianoap") + assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", model.FirstName, 3), "fabianoaps") + assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", model.FirstName, 4), "fabianoanps") + assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", model.FirstName, 16), "fabianoantunepereisouza") + assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", model.FirstName, 17), "fabianoantunepereirsouza") + assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", model.FirstName, 18), "fabianoantunespereirsouza") + assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", model.FirstName, 19), "fabianoantunespereirasouza") + assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", model.FirstName, 20), "fabianoantunespereirasouza") + assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", model.FirstName, 50), "fabianoantunespereirasouza") + + assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", model.LastName, 0), "fapsouza") + assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", model.LastName, 1), "faapsouza") + assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", model.LastName, 2), "faanpsouza") + assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", model.LastName, 3), "faanpesouza") + assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", model.LastName, 15), "fabianantunepereirsouza") + assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", model.LastName, 16), "fabianoantunepereirsouza") + assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", model.LastName, 17), "fabianoantunespereirsouza") + assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", model.LastName, 18), "fabianoantunespereirasouza") + assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", model.LastName, 19), "fabianoantunespereirasouza") + assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", model.LastName, 100), "fabianoantunespereirasouza") +} diff --git a/model/krb.go b/model/krb.go new file mode 100644 index 0000000..2c9493b --- /dev/null +++ b/model/krb.go @@ -0,0 +1,46 @@ +package model + +import ( + "fmt" + "os/exec" +) + +// creates a KerberosPrincipal for the user +func CreateKRBPrincipal(login string) error { + cmd := exec.Command("kadmin.local", "-q", + fmt.Sprintf("addprinc -policy padrao -randkey -x dn=uid=%s,ou=usuarios,dc=c3local %s", login, login)) + + output, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("Failed to add Kerberos principal: %v\nOutput: %s", err, output) + } + + return nil +} + +// command that changes the password >:D +// the command kadmin.local returns 0 if the password change +// fails, bruh. so we have to check differently. +// FIXME: maybe do the validation with regex? +func ModKRBPassword(login, password string) error { + cmd := exec.Command("kadmin.local", "-q", + fmt.Sprintf("cpw -pw %s %s", password, login)) + + output, _ := cmd.CombinedOutput() + if len(output) > 105 { + return fmt.Errorf("Error found changing password, output: \n%v", string(output[:])) + } + + return nil +} + +func DelKRBPrincipal(login string) error { + 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) + } + + return nil +} diff --git a/model/ldap.go b/model/ldap.go new file mode 100644 index 0000000..4e430b5 --- /dev/null +++ b/model/ldap.go @@ -0,0 +1,281 @@ +package model + +import ( + "fmt" + "os" + "sort" + "strconv" + "strings" + + "github.com/go-ldap/ldap/v3" + "gitlab.c3sl.ufpr.br/tss24/useradm/utils" +) + +const ( + PASSWD_PATH = "/etc/ldapscripts/ldapscripts.passwd" +) + +// stablishes a connection to LDAP +func ConnLDAP() (*ldap.Conn, error) { + // connect to the LDAP server + l, err := ldap.DialURL("ldapi:///") + if err != nil { + return nil, fmt.Errorf("Failed to connect to LDAP: %v", err) + } + + // get admin credentials + password, err := getPasswordLDAP(PASSWD_PATH) + if err != nil { + return nil, fmt.Errorf("Failed to read LDAP password: %v", err) + } + + // bind using admin credentials + err = l.Bind("cn=admin,dc=c3local", password) + if err != nil { + l.Close() + return nil, fmt.Errorf("Failed to bind with credentials: %v", err) + } + + return l, nil +} + +func getPasswordLDAP(path string) (string, error) { + passwd, err := os.ReadFile(path) + if err != nil { + err = fmt.Errorf("Error reading password file: %w", err) + return "", err + } + + password := strings.TrimSpace(string(passwd)) + + return password, nil +} + +// generates a LDAP request and adds the user to LDAP +func (u *User) AddToLDAP(l *ldap.Conn) error { + + 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("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 = AddToGroupLDAP(l, u.UID, u.GID) + if err != nil { + return err + } + + return nil +} + +func DelFromLDAP(UID string) error { + l, err := ConnLDAP() + if err != nil { + return err + } + defer l.Close() + + // search for all groups the user is a member of + searchReq := ldap.NewSearchRequest( + "ou=grupos,dc=c3local,dc=com", + ldap.ScopeWholeSubtree, + ldap.NeverDerefAliases, 0, 0, false, + "(memberUid="+UID+")", // Filter by UID membership + []string{"dn", "cn"}, + nil, + ) + + groups, err := l.Search(searchReq) + if err != nil && !ldap.IsErrorWithCode(err, ldap.LDAPResultNoSuchObject) { + return fmt.Errorf("Group members search failed: %v", err) + } + + // iterate and remove user from each group + for _, entry := range groups.Entries { + groupName := entry.GetAttributeValue("cn") + if err := DelFromGroupLDAP(l, UID, groupName); err != nil { + fmt.Printf("Warning: %v\n", err) + } + } + + // removing user entry + userDN := "uid=" + UID + "ou=usuarios,dc=c3local" + delReq := ldap.NewDelRequest(userDN, nil) + if err := l.Del(delReq); err != nil && + !ldap.IsErrorWithCode(err, ldap.LDAPResultNoSuchObject) { + return fmt.Errorf("User deletion failed: %v", err) + } + + return nil +} + +func DelFromGroupLDAP(l *ldap.Conn, userUID, GID string) error { + groupDN := fmt.Sprintf("cn=%s,ou=grupos,dc=c3local", GID) + modReq := ldap.NewModifyRequest(groupDN, nil) + modReq.Delete("memberUid", []string{userUID}) + + err := l.Modify(modReq) + if err != nil && !ldap.IsErrorWithCode(err, ldap.LDAPResultNoSuchAttribute) { + return fmt.Errorf("Failed to remove user %s from group %s: %v", userUID, groupDN, err) + } + + return nil +} + +// adds a user to a group in LDAP +func AddToGroupLDAP(l *ldap.Conn, UID, GID string) error { + groupDN := fmt.Sprintf("cn=%s,ou=grupos,dc=c3local", GID) + modReq := ldap.NewModifyRequest(groupDN, nil) + modReq.Add("memberUid", []string{UID}) + + err := l.Modify(modReq) + if err != nil { + return fmt.Errorf("Failed to add user %s to group %s: %v", UID, GID, err) + } + + return nil +} + +func GetAllUsersLDAP(l *ldap.Conn) ([]User, error) { + var users []User + + // create the LDAP search request + req := ldap.NewSearchRequest( + "dc=c3local", // search base + ldap.ScopeWholeSubtree, // scope + ldap.NeverDerefAliases, // aliases + 0, 0, // size/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(req) + 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 := GetAllGroupsLDAP(l) + 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 := 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: utils.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() + users = append(users, user) + } + + return users, nil +} + +func GetAllGroupsLDAP(l *ldap.Conn) (map[string]string, error) { + groupMap := make(map[string]string) + + // 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 GetAllUIDsLDAP(l *ldap.Conn) ([]int, error) { + var uidNumbers []int + + // 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 +} diff --git a/model/ltype.go b/model/ltype.go new file mode 100644 index 0000000..6c3eb10 --- /dev/null +++ b/model/ltype.go @@ -0,0 +1,40 @@ +package model + +import "strings" + +type LoginType int + +const ( + LoginTypeUnknown LoginType = iota - 1 // error value + Initials + FirstName + LastName +) + +// converts to string +func (lt LoginType) String() string { + switch lt { + case Initials: + return "Initials" + case FirstName: + return "FirstName" + case LastName: + return "LastName" + default: + return "Unknown" + } +} + +// converts to Ltype +func (lt *LoginType) Parse(s string) { + switch strings.ToLower(s) { + case "initials": + *lt = Initials + case "firstname": + *lt = FirstName + case "lastname": + *lt = LastName + default: + *lt = LoginTypeUnknown + } +} diff --git a/model/opts.go b/model/opts.go index a2f2768..a89fdce 100644 --- a/model/opts.go +++ b/model/opts.go @@ -12,6 +12,7 @@ type Opts struct { UID string Resp string Name string + Path string Nobkp string Ltype string Shell string @@ -30,28 +31,29 @@ type Opts struct { func (o *Opts) ToString() string { return fmt.Sprintf(`Opts: - GRR %s - GID %s - UID %s - Resp %s - Name %s - Nobkp %s - Ltype %s - Shell %s - Passwd %s - Webdir %s - Status %s - Expiry %s - Homedir %s + GRR %v + GID %v + UID %v + Resp %v + Name %v + Path %v + Nobkp %v + Ltype %v + Shell %v + Passwd %v + Webdir %v + Status %v + Expiry %v + Homedir %v Ignore %v Exact %v Block %v Unblock %v Confirm %v Number %v`, - o.GRR, o.GID, o.UID, o.Resp, o.Name, o.Nobkp, o.Ltype, o.Shell, - o.Passwd, o.Webdir, o.Status, o.Expiry, o.Homedir, o.Ignore, - o.Exact, o.Block, o.Unblock, o.Confirm, o.Number) + o.GRR, o.GID, o.UID, o.Resp, o.Name, o.Path, o.Nobkp, o.Ltype, + o.Shell, o.Passwd, o.Webdir, o.Status, o.Expiry, o.Homedir, + o.Ignore, o.Exact, o.Block, o.Unblock, o.Confirm, o.Number) } func (o *Opts) RetrieveOpts(cmd *cobra.Command) error { @@ -92,7 +94,12 @@ func (o *Opts) RetrieveOpts(cmd *cobra.Command) error { return err } - o.Webdir, err = getFlagString(cmd, "path") + o.Webdir, err = getFlagString(cmd, "web") + if err != nil { + return err + } + + o.Path, err = getFlagString(cmd, "path") if err != nil { return err } diff --git a/model/user.go b/model/user.go index 0fc75b1..ade5235 100644 --- a/model/user.go +++ b/model/user.go @@ -1,6 +1,34 @@ package model -import "fmt" +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "strconv" + "strings" + "time" + + "github.com/go-ldap/ldap/v3" + "gitlab.c3sl.ufpr.br/tss24/useradm/utils" +) + +const ( + MIN_UID = 1000 + MAX_UID = 6000 + MAX_VARIANCE = 30 + 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 + NUM_GECOS_FIELDS = 8 +) + +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 +) type User struct { DN string @@ -9,7 +37,7 @@ type User struct { GID string Name string Resp string - Ltype string + Ltype LoginType Gecos string Shell string Status string @@ -22,19 +50,244 @@ 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 + + defer func() { + if !success { + _ = DelKRBPrincipal(u.UID) + _ = DelFromLDAP(u.UID) + } + }() + + err := u.AddToLDAP(l) + if err != nil { + return err + } + + err = CreateKRBPrincipal(u.UID) + if err != nil { + return err + } + + err = ModKRBPassword(u.UID, u.Password) + if err != nil { + return err + } + + err = u.CreateDirs() + if err != nil { + return err + } + + success = true + return nil +} + +func Search(users []User, ig, ex bool, l, g, n, r, s, h string) []User { + return utils.Filter(users, func(u User) bool { + matches := func(src, target string) bool { + if ig { + src, target = strings.ToLower(src), strings.ToLower(target) + } + if ex { + return src == target // exact match + } + return strings.Contains(src, target) // partial match + } + + // search + return (r == "" || matches(u.GRR, r)) && + (n == "" || matches(u.Name, n)) && + (l == "" || matches(u.UID, l)) && + (g == "" || matches(u.GID, g)) && + (s == "" || matches(u.Status, s)) && + (h == "" || matches(u.Homedir, h)) + }) +} + +// searches for the specified login string, there can only be one >:) +func Locate(l *ldap.Conn, login string) (User, error) { + var u User + users, err := GetAllUsersLDAP(l) + if err != nil { + return u, err + } + + if !LoginExists(users, login) { + return u, fmt.Errorf("Failed to find login in LDAP database: %v", err) + } + + filter := Search(users, false, true, login, "", "", "", "", "") + if len(filter) != 1 { + return u, fmt.Errorf(`More than one user matched the login given. +search made: "useradm user show -l %v -e"`, login) + } + + u = filter[0] + return u, nil +} + +// DN formatting +func (u *User) SetDN(UID string) { + u.DN = "uid=" + UID + "ou=usuarios,dc=c3local" +} + +// finds next available uidNumber (MEX from uid group) +func (u *User) GetNewUIDNumber(l *ldap.Conn) error { + uids, err := GetAllUIDsLDAP(l) + 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") + } + + u.UIDNumber = strconv.Itoa(candidate) + return nil +} + +// creates the login, if it already exists try again with variance +func (u *User) GenUniqueUID(users []User) error { + used, variance := true, 0 + for used { + u.UID = u.genLogin(variance) + + used = LoginExists(users, u.UID) || utils.MailAliasExists(u.UID) + + variance++ + if variance > MAX_VARIANCE { + return fmt.Errorf("Could't generate login automatically, please inform the desired login") + } + } + return nil +} + +// ye +func (u *User) genLogin(variance int) string { + parts := utils.FormatName(u.Name) + if len(parts) == 0 || u.Ltype == LoginTypeUnknown { + return "_" + } + + partPrefixLen := make([]int, len(parts)) + if u.Ltype == Initials { + // the first letter of each part must appear + for i := 0; i < len(parts); i++ { + partPrefixLen[i] = 1 + } + } else if u.Ltype == FirstName { + // the first name (part) must appear + partPrefixLen[0] = len(parts[0]) + } else { + // the first letter of each part must appear + for i := 0; i < len(parts); i++ { + partPrefixLen[i] = 1 + } + // last part aswell + partPrefixLen[len(parts)-1] = len(parts[len(parts)-1]) + } + + partPrefixIx := 0 + for i := 0; i < variance; i++ { + ok := false + for k := 0; k < len(parts) && !ok; k++ { + if partPrefixLen[partPrefixIx] < len(parts[partPrefixIx]) { + partPrefixLen[partPrefixIx]++ + ok = true + } + partPrefixIx = (partPrefixIx + 1) % len(parts) + } + if !ok { + // it's joever, from now on nothing happens, quit :D + break + } + } + + // contruct the login with the given legths + login := "" + for i := 0; i < len(parts); i++ { + login += parts[i][:partPrefixLen[i]] + } + if login == "" { + return "_" + } + + if u.Ltype == Initials { + login += u.GRR[2:4] + } + return login +} + +func (u *User) GenGecos() { + u.Gecos = u.Name + "," + u.Gecos += u.GRR + "," + u.Gecos += u.Resp + "," + u.Gecos += u.Expiry + "," + u.Gecos += u.Status + "," + u.Gecos += u.Ltype.String() + "," + u.Gecos += u.Webdir + "," + u.Gecos += u.Nobackup +} + func (u *User) ToString() string { return fmt.Sprintf(`User: - Name: %s - Login: %s - GRR: %s - Group: %s - Shell: %s - Home: %s - Webdir: %s - Nobkp: %s - Status: %s - Resp: %s - Expiry: %s`, + Name: %v + Login: %v + GRR: %v + Group: %v + Shell: %v + Home: %v + Webdir: %v + Nobkp: %v + Status: %v + Resp: %v + Expiry: %v`, u.Name, u.UID, u.GRR, u.GID, u.Shell, u.Homedir, u.Webdir, u.Nobackup, u.Status, u.Resp, u.Expiry) } @@ -42,24 +295,177 @@ func (u *User) ToString() string { // useful for debugging :) func (u *User) FullToString() string { return fmt.Sprintf(`User: - DN: %s - GRR: %s - UID: %s - GID: %s - Name: %s - Resp: %s - Ltype: %s - Gecos: %s - Shell: %s - Status: %s - Webdir: %s - Expiry: %s - Homedir: %s - Password: %s - Nobackup: %s - GIDNumber: %s - UIDNumber: %s`, + DN: %v + GRR: %v + UID: %v + GID: %v + Name: %v + Resp: %v + Ltype: %v + Gecos: %v + Shell: %v + Status: %v + Webdir: %v + Expiry: %v + Homedir: %v + Password: %v + Nobackup: %v + GIDNumber: %v + UIDNumber: %v`, u.DN, u.GRR, u.UID, u.GID, u.Name, u.Resp, u.Ltype, u.Gecos, u.Shell, u.Status, u.Webdir, u.Expiry, u.Homedir, u.Password, u.Nobackup, u.GIDNumber, u.UIDNumber) } + +func LoginExists(users []User, login string) bool { + return utils.Exists(users, func(u User) bool { return u.UID == login }) +} + +// 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 }) || + utils.Exists(users, func(u User) bool { return u.GRR == "GRR"+grr }) +} + +// moves dirs to their respective trash dir +func (u *User) DirsToTrash() error { + err := utils.MoveAndChown(u.Homedir, HOME_TRASH, "nobody", "nogroup") + if err != nil { + return err + } + + err = utils.MoveAndChown(u.Nobackup, NO_BKP_TRASH, "nobody", "nogroup") + if err != nil { + return err + } + + err = utils.MoveAndChown(u.Webdir, WEB_TRASH, "nobody", "nogroup") + if err != nil { + return err + } + + return nil +} + +func (u *User) CreateDirs() error { + success := false + + defer func() { + if !success { + fmt.Println("Error found creating dirs, cleaning up...") + _ = os.RemoveAll(u.Nobackup) + _ = os.RemoveAll(u.Homedir) + } + }() + + if err := u.CreateHome(u.Homedir); err != nil { + return err + } + + if err := u.CreateHome(u.Nobackup); err != nil { + return err + } + + if err := u.CreateWeb(); err != nil { + return err + } + + success = true + return nil +} + +func (u *User) CreateHome(homeDir string) error { + perm := DEF_PERMISSION + if u.Status == "Blocked" { + perm = BLK_PERMISSION + } + + // create directory + cmd := exec.Command("mkdir", "-p", homeDir) + cmd.Stdout = nil + cmd.Stderr = nil + if err := cmd.Run(); err != nil { + return fmt.Errorf("Failed to create home directory: %w", err) + } + + // copy /etc/skel + cmd = exec.Command("cp", "-r", "/etc/skel/.", homeDir) + if err := cmd.Run(); err != nil { + return fmt.Errorf("Failed to copy /etc/skel contents: %w", err) + } + + // change permissions + 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) + if err := cmd.Run(); err != nil { + return fmt.Errorf("Failed to change ownership: %w", err) + } + + return nil +} + +func (u *User) CreateWeb() error { + success := false + + if u.Webdir == "_" { + success = true + return nil + } + + perm := DEF_PERMISSION + if u.Status == "Blocked" { + perm = BLK_PERMISSION + } + + 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 { + return fmt.Errorf("Failed to create web directory: %w", err) + } + + // create index + cmd = exec.Command("touch", filepath.Join(u.Webdir, "index.html")) + if err := cmd.Run(); err != nil { + return fmt.Errorf("Failed to create index.html: %w", err) + } + + // 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) + 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) + if err := cmd.Run(); err != nil { + return fmt.Errorf("Failed to change ownership: %w", err) + } + + // 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 +} diff --git a/utils/utils.go b/utils/utils.go new file mode 100644 index 0000000..464987a --- /dev/null +++ b/utils/utils.go @@ -0,0 +1,197 @@ +package utils + +import ( + "bufio" + "crypto/rand" + "fmt" + "os" + "os/exec" + "path/filepath" + "strconv" + "strings" + "time" +) + +const ( + MAIL_ALIAS_FILE = "/etc/aliases" + PASSWD_PATH = "/etc/ldapscripts/ldapscripts.passwd" +) + +// 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 +} + +// 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 +} + +// 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} + splitName := strings.Fields(name) + var parts []string + + for _, part := range splitName { + lowerPart := strings.ToLower(part) + if !connectives[lowerPart] { + parts = append(parts, string(lowerPart)) + } + } + return parts +} + +// 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 +} + +func GetGIDNumFromGID(groups map[string]string, GID string) (string, error) { + for key, val := range groups { + if val == GID { + return key, nil + } + } + return "", fmt.Errorf("Couldn't find group GIDNumber") +} + +func GenDirPath(base, group, login, input string) (string, error) { + if input != "" { + return input, nil + } + p := filepath.Join(base, group, login) + + if PathExists(p) { + return p, fmt.Errorf("Path already exists") + } + + return p, nil +} + +func PathExists(path string) bool { + _, err := os.Stat(path) + if err == nil { + return true + } + return false +} + +// 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) +} + +func IfThenElse(condition bool, a string, b string) string { + if condition { + return a + } + return b +} + +func MoveAndChown(orig, dest, owner, group string) error { + // check if orig exists + if _, err := os.Stat(orig); err != nil { + fmt.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) + } + + // move directory + cmd := exec.Command("mv", orig, destPath) + if output, err := cmd.CombinedOutput(); err != nil { + return fmt.Errorf("Failed to move dirs: %w\nOutput: %v", err, output) + } + + // recursive chown + 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 +} + +// 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) + + reader := bufio.NewReader(os.Stdin) + response, _ := reader.ReadString('\n') + + if strings.TrimSpace(strings.ToLower(response)) != "y" { + fmt.Fprintln(os.Stderr, "Aborted.") + os.Exit(1) + } + } +} + +// LDAP has a cache... +func ClearCache() error { + cmd := exec.Command("nscd", "-i", "passwd") + + cmd.Stdout = nil + cmd.Stderr = nil + err := cmd.Run() + if err != nil { + return err + } + + cmd = exec.Command("nscd", "-i", "group") + return cmd.Run() +} + +func IsValidDate(arr []string) bool { + if len(arr) != 3 { + return false + } + + // convert to int + 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 { + return false + } + + // ensure year is two digits + if year < 0 || year > 99 { + return false + } + + // validate the date + fullYear := 2000 + year + t := time.Date(fullYear, time.Month(mth), day, 0, 0, 0, 0, time.UTC) + return t.Day() == day && t.Month() == time.Month(mth) +} -- GitLab