diff --git a/cmd/user.go b/cmd/user.go index 2a4dafd010904b779e5d603cd3ef51c5e0ce8acc..177e3763355b9e5b2f0e96d1db650da8ed43d575 100644 --- a/cmd/user.go +++ b/cmd/user.go @@ -18,4 +18,5 @@ func init() { userCmd.AddCommand(user.DeleteUserCmd) userCmd.AddCommand(user.ModCmd) userCmd.AddCommand(user.ShowCmd) + userCmd.AddCommand(user.ResetCmd) } diff --git a/cmd/user/create.go b/cmd/user/create.go index 26b41cb63f0cbd48ecbbf25ff6b0ff26f17cefed..dee6c40ee46eb1724411d9b6c232f1f2d0cc1735 100644 --- a/cmd/user/create.go +++ b/cmd/user/create.go @@ -20,7 +20,7 @@ const ( 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 + MAX_VARIANCE = 200 // tries made to create login automatically MIN_UID = 1000 MAX_UID = 6000 ) @@ -239,42 +239,44 @@ func genLogin(name string, grr string, ltype LoginType, variance int) string { return "_" } - part_prefix_len := make([]int, len(parts)) + partPrefixLen := make([]int, len(parts)) if ltype == Initials { - // a primeira letra de cada parte deve aparecer + // the first letter of each part must appear for i := 0; i < len(parts); i++ { - part_prefix_len[i] = 1 + partPrefixLen[i] = 1 } } else if ltype == FirstName { - // a primeira parte inteira deve aparecer - part_prefix_len[0] = len(parts[0]) + // the first name(part) must appear + partPrefixLen[0] = len(parts[0]) } else { - // a primeira letra de cada parte deve aparecer + // the first letter of each part must appear for i := 0; i < len(parts); i++ { - part_prefix_len[i] = 1 + partPrefixLen[i] = 1 } - // assim com a parte final do nome - part_prefix_len[len(parts)-1] = len(parts[len(parts)-1]) + // just as the last part + partPrefixLen[len(parts)-1] = len(parts[len(parts)-1]) } - part_prefix_ix := 0 + + partPrefixIx := 0 for i := 0; i < variance; i++ { ok := false for k := 0; k < len(parts) && !ok; k++ { - if part_prefix_len[part_prefix_ix] < len(parts[part_prefix_ix]) { - part_prefix_len[part_prefix_ix]++ + if partPrefixLen[partPrefixIx] < len(parts[partPrefixIx]) { + partPrefixLen[partPrefixIx]++ ok = true } - part_prefix_ix = (part_prefix_ix + 1) % len(parts) + partPrefixIx = (partPrefixIx + 1) % len(parts) } if !ok { - // acabou o que fazer, vamos abandonar porque não vai mudar mais nada + // 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][:part_prefix_len[i]] + login += parts[i][:partPrefixLen[i]] } if login == "" { return "_" @@ -396,21 +398,6 @@ func addKerberosPrincipal(login string) error { return nil } -// sets the password for the user (via kerberos) -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 - output, err := cmd.CombinedOutput() - if err != nil { - return fmt.Errorf("Failed to change password for %s: %v\nOutput: %s", login, err, output) - } - - return nil -} - func createUserDirs(u model.User) error { success := false diff --git a/cmd/user/mod.go b/cmd/user/mod.go index 74f23bbd96101cc57ca086237eb303d6f309692b..ded2fa745cf200b6fa522aaa3b53a7c93ee90f71 100644 --- a/cmd/user/mod.go +++ b/cmd/user/mod.go @@ -2,31 +2,32 @@ package user import ( "bufio" - "crypto/rand" "fmt" "os" "os/exec" "strings" + "github.com/go-ldap/ldap/v3" "github.com/spf13/cobra" "gitlab.c3sl.ufpr.br/tss24/useradm/model" "gopkg.in/yaml.v3" ) 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", + Long: "Opens a file for editing the users config. Uses $EDITOR variable", Args: cobra.ExactArgs(1), RunE: modifyUserFunc, } @@ -44,41 +45,144 @@ func modifyUserFunc(cmd *cobra.Command, args []string) error { err = fmt.Errorf("More than one user found") return err } - u := res[0] + curr := 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, + Name: curr.Name, + GRR: curr.GRR, + Group: curr.GID, + Status: curr.Status, + Shell: curr.Shell, + Course: curr.Course, + Resp: curr.Resp, + Expiry: curr.Expiry, } - // Criar arquivo temporário + changes, err := promptUserYaml(state) + + err = validateGRR(changes.GRR) + if err != nil { + return err + } + + err = validateGID(changes.Group) + if err != nil { + return err + } + + err = validateStatus(changes.Status) + if err != nil { + return err + } + + err = validateExpiry(changes.Expiry) + if err != nil { + return err + } + + req, err := genRequest(curr, changes) + if err != nil { + return err + } + + l, err := connLDAP() + if err != nil { + return err + } + defer l.Close() + + if err := l.Modify(req); err != nil { + return fmt.Errorf("Failed to update user attributes: %v", err) + } + + fmt.Printf("Changes applied!\n") + return nil +} + +func genRequest(curr model.User, changes cfg) (*ldap.ModifyRequest, error) { + req := ldap.NewModifyRequest(curr.DN, nil) + + // changed name + if curr.Name != changes.Name { + req.Replace("cn", []string{changes.Name}) + } + + // changed status + if curr.Status != changes.Status { + if changes.Status == "Active" && changes.Shell == "/bin/false" { + req.Replace("loginShell", []string{"/bin/bash"}) + } else if changes.Status == "Active" { + req.Replace("loginShell", []string{changes.Shell}) + } else { + req.Replace("loginShell", []string{"/bin/false"}) + } + } else { + // only changed shell + if curr.Shell != changes.Shell { + req.Replace("loginShell", []string{changes.Shell}) + } + } + + // 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 { + gidNumber = id + break + } + } + + req.Replace("gidNumber", []string{gidNumber}) + } + + changed := applyChangesToUser(curr, changes) + + changed = genGecos(changed) + + // changed gecos + if curr.Gecos != changed.Gecos { + req.Replace("gecos", []string{changed.Gecos}) + } + + return req, nil +} + +func applyChangesToUser(c model.User, n cfg) model.User { + c.GRR = n.GRR + c.Name = n.Name + c.Resp = n.Resp + c.Shell = n.Shell + c.Status = n.Status + c.Course = n.Course + c.Expiry = n.Expiry + return c +} + +func promptUserYaml(state cfg) (cfg, error) { + var newState cfg tmpFile, err := os.CreateTemp("", "config-*.yaml") if err != nil { err = fmt.Errorf("Error trying to create temp file: %v", err) - return err + return newState, err } defer os.Remove(tmpFile.Name()) - // Serializar apenas os campos editáveis data, err := yaml.Marshal(state) if err != nil { - err = fmt.Errorf("Error serializing the yaml: %v", err) - return err + return newState, fmt.Errorf("Error serializing the yaml: %v", err) } - // Escrever no arquivo temporário if err := os.WriteFile(tmpFile.Name(), data, 0644); err != nil { - err = fmt.Errorf("Error writing to temp file: %v", err) - return err + return newState, fmt.Errorf("Error writing to temp file: %v", err) } - // Abrir no editor + // abrir o editor editor := os.Getenv("EDITOR") if editor == "" { editor = "nano" @@ -90,42 +194,19 @@ 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: %v", err) - return err + return newState, fmt.Errorf("Error opening the editor: %v", err) } - // Ler o conteúdo editado editedData, err := os.ReadFile(tmpFile.Name()) if err != nil { - err = fmt.Errorf("Error reading the modified file: %v", err) - return err + return newState, fmt.Errorf("Error reading the modified file: %v", 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: %v", err) - return err + return newState, fmt.Errorf("Error de-serializing the yaml: %v", err) } - // Exibir a struct atualizada - fmt.Println("New config:") - fmt.Printf("%+v\n", newState) - - 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) + return newState, nil } // prints a confirmation prompt, given the operation being performed diff --git a/cmd/user/reset.go b/cmd/user/reset.go new file mode 100644 index 0000000000000000000000000000000000000000..a84d944a94bde82206b9ee79c2cab15d53e9a0f6 --- /dev/null +++ b/cmd/user/reset.go @@ -0,0 +1,79 @@ +package user + +import ( + "crypto/rand" + "fmt" + "os/exec" + + "github.com/spf13/cobra" +) + +var ResetCmd = &cobra.Command{ + Use: "reset [username]", + Short: "Reset the password of a user", + Args: cobra.ExactArgs(1), + RunE: resetPass, +} + +func init() { + ResetCmd.Flags().StringP("passwd", "p", "", "User's new password") +} + +func resetPass(cmd *cobra.Command, args []string) error { + pass, err := cmd.Flags().GetString("passwd") + if err != nil { + return err + } + + users, err := getUsers() + if err != nil { + return err + } + + login := args[0] + res := searchUser(users, false, login, "", "", "", "", "") + if len(res) != 1 { + return fmt.Errorf("More than one user found") + } + + if pass == "" { + pass = genPassword() + } + + err = modKerberosPassword(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 +} + +// 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) +} + +// command that changes the password >:D +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 + output, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("Failed to change password for %s: %v\nOutput: %s", login, err, output) + } + + return nil +}