From 991b71f46c167b1748e545de7f47dd193f81b7e0 Mon Sep 17 00:00:00 2001
From: tss24 <tss24@inf.ufpr.br>
Date: Thu, 27 Feb 2025 19:02:23 -0300
Subject: [PATCH] Finish bulk prototype and remove course option

---
 README.md          |   8 +-
 cmd/user.go        |   5 +-
 cmd/user/bulk.go   | 178 +++++++++++++++++++++++++++++++++++++++++++++
 cmd/user/create.go |  73 ++++++++++---------
 cmd/user/mod.go    |  19 +----
 cmd/user/remove.go |  17 +++--
 cmd/user/show.go   |  24 +++---
 model/opts.go      |  32 +++++---
 model/user.go      |   9 +--
 9 files changed, 272 insertions(+), 93 deletions(-)
 create mode 100644 cmd/user/bulk.go

diff --git a/README.md b/README.md
index e24a67a..a44b3a8 100644
--- a/README.md
+++ b/README.md
@@ -48,16 +48,16 @@ To create a user you can use:
 
 the command needs some basic info to create a user. The baseline is this:
 
-    useradm user create -n "<User Full Name>" -g <User group> -c <User course>
+    useradm user create -n "<User Full Name>" -g <User group>
 
 but if we run something like 
 
-    useradm user create -n "Joao da Silva" -g bcc -c bcc
+    useradm user create -n "Joao da Silva" -g bcc
 
 we will get an error. That is because the login generation needs the user GRR 
 for the default 'ini' login type. So this is now correct:
 
-    useradm user create -n "Joao da Silva" -g bcc -c bcc -t last
+    useradm user create -n "Joao da Silva" -g bcc -t last
 
 the auto generation will be explained in the section "Implementation"
 
@@ -99,8 +99,6 @@ To reset a user's password you can do:
 with no flag a password will be auto-generated. Passing the flag -p you can
 specify the new password. Passwords need to be good lol.
 
-Note: Check "Implementation" to learn about kadmin.local command 
-
 ### Bulk
 
 TODO :))))))
diff --git a/cmd/user.go b/cmd/user.go
index 0b3ae43..21cfa72 100644
--- a/cmd/user.go
+++ b/cmd/user.go
@@ -14,9 +14,10 @@ new users, please do so with the bulk subcommand.`,
 }
 
 func init() {
-	userCmd.AddCommand(user.CreateUserCmd)
-	userCmd.AddCommand(user.RemoveUserCmd)
 	userCmd.AddCommand(user.ModCmd)
 	userCmd.AddCommand(user.ShowCmd)
+	userCmd.AddCommand(user.BulkCmd)
 	userCmd.AddCommand(user.ResetCmd)
+	userCmd.AddCommand(user.CreateCmd)
+	userCmd.AddCommand(user.RemoveCmd)
 }
diff --git a/cmd/user/bulk.go b/cmd/user/bulk.go
new file mode 100644
index 0000000..9b45291
--- /dev/null
+++ b/cmd/user/bulk.go
@@ -0,0 +1,178 @@
+package user
+
+// TODO: PQ Q N FUNCIONA??????????????
+import (
+	"fmt"
+	"os"
+	"strconv"
+
+	"github.com/spf13/cobra"
+	"gitlab.c3sl.ufpr.br/tss24/useradm/model"
+)
+
+var BulkCmd = &cobra.Command{
+	Use:   "bulk",
+	Short: "Create a lot of similar users",
+	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.MarkFlagRequired("login")
+	BulkCmd.MarkFlagRequired("passwd")
+	BulkCmd.MarkFlagRequired("group")
+	BulkCmd.MarkFlagRequired("number")
+}
+
+func bulkCreate(cmd *cobra.Command, args []string) error {
+	var opts model.Opts
+	var users []model.User
+
+	users, err := getUsers()
+	if err != nil {
+		return err
+	}
+
+	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 + "#",
+	}
+
+	base.Homedir, err = genDirPath("/home", base.GID, base.UID, "")
+	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)
+		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
+}
diff --git a/cmd/user/create.go b/cmd/user/create.go
index a7bd49e..7b4c9b5 100644
--- a/cmd/user/create.go
+++ b/cmd/user/create.go
@@ -23,7 +23,7 @@ const (
 	MAX_UID         = 6000
 )
 
-var CreateUserCmd = &cobra.Command{
+var CreateCmd = &cobra.Command{
 	Use:   "create",
 	Short: "Create new user",
 	RunE:  createUserFunc,
@@ -32,28 +32,25 @@ var CreateUserCmd = &cobra.Command{
 func init() {
 	// possible flags
 	// FIXME: maybe leave less flags for user input
-	CreateUserCmd.Flags().StringP("grr", "r", "_", "User GRR, required for ini type")
-	CreateUserCmd.Flags().StringP("type", "t", "ini", "Type of auto-generated login: ini, first or last")
-	CreateUserCmd.Flags().StringP("path", "w", "", "Full path to webdir, /home/html/inf/login if empty")
-	CreateUserCmd.Flags().StringP("name", "n", "_", "User full name, required, use quotes for spaces")
-	CreateUserCmd.Flags().StringP("login", "l", "", "User login name, auto-generated if empty")
-	CreateUserCmd.Flags().StringP("group", "g", "", "User base group, required")
-	CreateUserCmd.Flags().StringP("shell", "s", "/bin/bash", "Full path to shell")
-	CreateUserCmd.Flags().StringP("homedir", "d", "", "Home directory path, /home/group/login if empty")
-	CreateUserCmd.Flags().StringP("passwd", "p", "", "User password, auto-generated if empty")
-	CreateUserCmd.Flags().StringP("status", "a", "Active", "User status, Active or Blocked")
-	CreateUserCmd.Flags().StringP("resp", "i", "_", "Person responsible for the account")
-	CreateUserCmd.Flags().StringP("expiry", "e", "_", "Expiry date in format dd.mm.yy")
-	CreateUserCmd.Flags().StringP("course", "c", "_", "User course/minicourse, even temp ones")
-	CreateUserCmd.Flags().StringP("nobkp", "b", "", "User nobackup directory path")
+	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("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")
+	CreateCmd.Flags().StringP("shell", "s", "/bin/bash", "Full path to shell")
+	CreateCmd.Flags().StringP("homedir", "d", "", "Home directory path, /home/group/login if empty")
+	CreateCmd.Flags().StringP("passwd", "p", "", "User password, auto-generated if empty")
+	CreateCmd.Flags().StringP("status", "a", "Active", "User status, Active or Blocked")
+	CreateCmd.Flags().StringP("resp", "i", "_", "Person responsible for the account")
+	CreateCmd.Flags().StringP("expiry", "e", "_", "Expiry date in format dd.mm.yy")
+	CreateCmd.Flags().StringP("nobkp", "b", "", "User nobackup directory path")
 
 	// required flags
-	CreateUserCmd.MarkFlagRequired("name")
-	CreateUserCmd.MarkFlagRequired("group")
-	// made it required, may be overkill...
-	CreateUserCmd.MarkFlagRequired("course")
+	CreateCmd.MarkFlagRequired("name")
+	CreateCmd.MarkFlagRequired("group")
 
-	CreateUserCmd.Flags().BoolP("confirm", "y", false, "Skip confirmation prompt")
+	CreateCmd.Flags().BoolP("confirm", "y", false, "Skip confirmation prompt")
 }
 
 func createUserFunc(cmd *cobra.Command, args []string) error {
@@ -67,7 +64,7 @@ func createUserFunc(cmd *cobra.Command, args []string) error {
 	defer func() {
 		if !success {
 			_ = delKerberosPrincipal(usr.UID)
-			_ = delUserLDAP(usr)
+			_ = delUserLDAP(usr.UID)
 		}
 	}()
 
@@ -173,28 +170,24 @@ func createNewUserModel(cmd *cobra.Command) (model.User, bool, error) {
 		Shell:    opts.Shell,
 		Status:   opts.Status,
 		Expiry:   opts.Expiry,
-		Course:   opts.Course,
 		Webdir:   opts.Webdir,
 		Homedir:  opts.Homedir,
 		Nobackup: opts.Nobkp,
 		Password: ifThenElse(opts.Passwd != "", opts.Passwd, "[auto-generate]"),
 	}
 
-	u = genGecos(u)
+	u.Gecos = genGecos(u)
 
 	// get a new UIDNumber
-	newUIDNumber, err := getNewUIDNumber()
+	u.UIDNumber, err = getNewUIDNumber()
 	if err != nil {
 		return u, false, fmt.Errorf("failed to generate new UIDNumber for user: %v", err)
 	}
-	u.UIDNumber = newUIDNumber
 
 	// assign GIDNumber by traversing the groups
-	for key, val := range groups {
-		if val == u.GID {
-			u.GIDNumber = key
-			break
-		}
+	u.GIDNumber, err = findGIDNumber(groups, u.GID)
+	if err != nil {
+		return u, false, err
 	}
 
 	u.DN = "uid=" + u.UID + ",ou=usuarios,dc=c3local"
@@ -210,18 +203,16 @@ func genDirPath(base, group, login, input string) (string, error) {
 	return p, validatePath(p)
 }
 
-func genGecos(u model.User) model.User {
+func genGecos(u model.User) string {
 	gecos := u.Name + ","
 	gecos += u.GRR + ","
 	gecos += u.Resp + ","
-	gecos += u.Course + ","
 	gecos += u.Expiry + ","
 	gecos += u.Status + ","
 	gecos += u.Ltype + ","
 	gecos += u.Webdir + ","
 	gecos += u.Nobackup
-	u.Gecos = gecos
-	return u
+	return gecos
 }
 
 type LoginType int
@@ -328,6 +319,15 @@ func genUniqueUID(name, grr string, ltypeString string, users []model.User) (str
 	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)
@@ -482,6 +482,11 @@ func createHome(u model.User, homeDir string) error {
 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
diff --git a/cmd/user/mod.go b/cmd/user/mod.go
index 6792f92..2c06d20 100644
--- a/cmd/user/mod.go
+++ b/cmd/user/mod.go
@@ -19,7 +19,6 @@ type cfg struct {
 	Group  string `yaml:"Group"`
 	Status string `yaml:"Status"`
 	Shell  string `yaml:"Shell"`
-	Course string `yaml:"Course"`
 	Resp   string `yaml:"Resp"`
 	Expiry string `yaml:"Expiry"`
 }
@@ -37,25 +36,17 @@ func init() {
 }
 
 func modifyUserFunc(cmd *cobra.Command, args []string) error {
-	var users []model.User
 	var opts model.Opts
-	users, err := getUsers()
-	if err != nil {
-		return err
-	}
 
-	err = opts.RetrieveOpts(cmd)
+	err := opts.RetrieveOpts(cmd)
 	if err != nil {
 		return err
 	}
 
-	login := args[0]
-	res := searchUser(users, false, true, login, "", "", "", "", "")
-	if len(res) != 1 {
-		err = fmt.Errorf("More than one user found")
+	curr, err := locateUser(args[0])
+	if err != nil {
 		return err
 	}
-	curr := res[0]
 
 	state := cfg{
 		Name:   curr.Name,
@@ -63,7 +54,6 @@ func modifyUserFunc(cmd *cobra.Command, args []string) error {
 		Group:  curr.GID,
 		Status: curr.Status,
 		Shell:  curr.Shell,
-		Course: curr.Course,
 		Resp:   curr.Resp,
 		Expiry: curr.Expiry,
 	}
@@ -177,7 +167,7 @@ func genRequest(curr model.User, changes cfg) (*ldap.ModifyRequest, error) {
 
 	changed := applyChangesToUser(curr, changes)
 
-	changed = genGecos(changed)
+	changed.Gecos = genGecos(changed)
 
 	// changed gecos
 	if curr.Gecos != changed.Gecos {
@@ -194,7 +184,6 @@ func applyChangesToUser(c model.User, n cfg) model.User {
 	c.Resp = n.Resp
 	c.Shell = n.Shell
 	c.Status = n.Status
-	c.Course = n.Course
 	c.Expiry = n.Expiry
 	return c
 }
diff --git a/cmd/user/remove.go b/cmd/user/remove.go
index 499c5b3..0c10753 100644
--- a/cmd/user/remove.go
+++ b/cmd/user/remove.go
@@ -21,7 +21,7 @@ var (
 	WEB_TRASH    = "/home/contas_removidas/html/" + ANO
 )
 
-var RemoveUserCmd = &cobra.Command{
+var RemoveCmd = &cobra.Command{
 	Use:   "remove [username]",
 	Short: "Delete a user",
 	Args:  cobra.ExactArgs(1),
@@ -29,7 +29,7 @@ var RemoveUserCmd = &cobra.Command{
 }
 
 func init() {
-	RemoveUserCmd.Flags().BoolP("confirm", "y", false, "Skip confirmation prompt")
+	RemoveCmd.Flags().BoolP("confirm", "y", false, "Skip confirmation prompt")
 }
 
 func removeUserFunc(cmd *cobra.Command, args []string) error {
@@ -68,7 +68,7 @@ func removeUserFunc(cmd *cobra.Command, args []string) error {
 		return err
 	}
 
-	err = delUserLDAP(u)
+	err = delUserLDAP(u.UID)
 	if err != nil {
 		return err
 	}
@@ -91,7 +91,7 @@ func locateUser(login string) (model.User, error) {
 	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"`, login)
+search made: "useradm user show -l %v -e"`, login)
 	}
 
 	u = filter[0]
@@ -118,7 +118,7 @@ func removeDirs(u model.User) error {
 	return nil
 }
 
-func delUserLDAP(u model.User) error {
+func delUserLDAP(UID string) error {
 	l, err := connLDAP()
 	if err != nil {
 		return err
@@ -130,7 +130,7 @@ func delUserLDAP(u model.User) error {
 		"ou=grupos,dc=c3local,dc=com",
 		ldap.ScopeWholeSubtree,
 		ldap.NeverDerefAliases, 0, 0, false,
-		"(memberUid="+u.UID+")", // Filter by UID membership
+		"(memberUid="+UID+")", // Filter by UID membership
 		[]string{"dn", "cn"},
 		nil,
 	)
@@ -143,13 +143,14 @@ func delUserLDAP(u model.User) error {
 	// iterate and remove user from each group
 	for _, entry := range groups.Entries {
 		groupName := entry.GetAttributeValue("cn")
-		if err := delUserFromGroupLDAP(l, u.UID, groupName); err != nil {
+		if err := delUserFromGroupLDAP(l, UID, groupName); err != nil {
 			log.Printf("Warning: %v", err)
 		}
 	}
 
 	// removing user entry
-	delReq := ldap.NewDelRequest(u.DN, nil)
+	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)
diff --git a/cmd/user/show.go b/cmd/user/show.go
index 6de2eee..5e9ad4f 100644
--- a/cmd/user/show.go
+++ b/cmd/user/show.go
@@ -214,13 +214,12 @@ func getLDAPPassword(path string) (string, error) {
 // 0. users full name
 // 1. users grr
 // 2. responsible professor
-// 3. users course/minicourse (even temp)
-// 4. account expiry date (may be used for deletion)
-// 5. account status (Blocked/Active)
-// 6. login type (ini, first or last)
-// 7. users webdir
-// 8. users nobackup dir
-// 9. to be continued...
+// 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 {
@@ -236,15 +235,14 @@ func parseGecos(user model.User) model.User {
 
 	user.GRR = result[1]
 	user.Resp = result[2]
-	user.Course = result[3]
-	user.Expiry = result[4]
+	user.Expiry = result[3]
 	// only set with gecos if empty
 	if user.Status == "_" {
-		user.Status = result[5]
+		user.Status = result[4]
 	}
-	user.Ltype = result[6]
-	user.Webdir = result[7]
-	user.Nobackup = result[8]
+	user.Ltype = result[5]
+	user.Webdir = result[6]
+	user.Nobackup = result[7]
 
 	return user
 }
diff --git a/model/opts.go b/model/opts.go
index 93d947a..a2f2768 100644
--- a/model/opts.go
+++ b/model/opts.go
@@ -18,7 +18,6 @@ type Opts struct {
 	Passwd  string
 	Webdir  string
 	Status  string
-	Course  string
 	Expiry  string
 	Homedir string
 	Ignore  bool
@@ -26,6 +25,7 @@ type Opts struct {
 	Block   bool
 	Unblock bool
 	Confirm bool
+	Number  int
 }
 
 func (o *Opts) ToString() string {
@@ -41,17 +41,17 @@ func (o *Opts) ToString() string {
 	Passwd   %s
 	Webdir   %s
 	Status   %s
-	Course   %s
 	Expiry   %s
 	Homedir  %s
 	Ignore   %v
 	Exact    %v
 	Block    %v
 	Unblock  %v
-	Confirm  %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.Course, o.Expiry, o.Homedir, o.Ignore,
-		o.Exact, o.Block, o.Unblock, o.Confirm)
+		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 {
@@ -107,11 +107,6 @@ func (o *Opts) RetrieveOpts(cmd *cobra.Command) error {
 		return err
 	}
 
-	o.Course, err = getFlagString(cmd, "course")
-	if err != nil {
-		return err
-	}
-
 	o.Expiry, err = getFlagString(cmd, "expiry")
 	if err != nil {
 		return err
@@ -152,6 +147,11 @@ func (o *Opts) RetrieveOpts(cmd *cobra.Command) error {
 		return err
 	}
 
+	o.Number, err = getFlagInt(cmd, "number")
+	if err != nil {
+		return err
+	}
+
 	return nil
 }
 
@@ -172,6 +172,18 @@ func getFlagString(cmd *cobra.Command, flag string) (string, error) {
 	return flagValue, nil
 }
 
+func getFlagInt(cmd *cobra.Command, flag string) (int, error) {
+	if cmd.Flags().Lookup(flag) == nil {
+		return 0, nil
+	}
+
+	flagValue, err := cmd.Flags().GetInt(flag)
+	if err != nil {
+		return 0, fmt.Errorf("failed to get flag %q: %w", flag, err)
+	}
+	return flagValue, nil
+}
+
 func getFlagBool(cmd *cobra.Command, flag string) (bool, error) {
 	if cmd.Flags().Lookup(flag) == nil {
 		return false, nil
diff --git a/model/user.go b/model/user.go
index 1bce7ae..0fc75b1 100644
--- a/model/user.go
+++ b/model/user.go
@@ -12,7 +12,6 @@ type User struct {
 	Ltype     string
 	Gecos     string
 	Shell     string
-	Course    string
 	Status    string
 	Webdir    string
 	Expiry    string
@@ -35,10 +34,9 @@ func (u *User) ToString() string {
 	Nobkp:   %s
 	Status:  %s
 	Resp:    %s
-	Course:  %s
 	Expiry:  %s`,
 		u.Name, u.UID, u.GRR, u.GID, u.Shell, u.Homedir, u.Webdir,
-		u.Nobackup, u.Status, u.Resp, u.Course, u.Expiry)
+		u.Nobackup, u.Status, u.Resp, u.Expiry)
 }
 
 // useful for debugging :)
@@ -53,7 +51,6 @@ func (u *User) FullToString() string {
 	Ltype:     %s
 	Gecos:     %s
 	Shell:     %s
-	Course:    %s
 	Status:    %s
 	Webdir:    %s
 	Expiry:    %s
@@ -63,6 +60,6 @@ func (u *User) FullToString() string {
 	GIDNumber: %s
 	UIDNumber: %s`,
 		u.DN, u.GRR, u.UID, u.GID, u.Name, u.Resp, u.Ltype, u.Gecos,
-		u.Shell, u.Course, u.Status, u.Webdir, u.Expiry, u.Homedir,
-		u.Password, u.Nobackup, u.GIDNumber, u.UIDNumber)
+		u.Shell, u.Status, u.Webdir, u.Expiry, u.Homedir, u.Password,
+		u.Nobackup, u.GIDNumber, u.UIDNumber)
 }
-- 
GitLab