File: user.go

package info (click to toggle)
golang-github-tredoe-osutil 1.5.0-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 476 kB
  • sloc: makefile: 4
file content (395 lines) | stat: -rw-r--r-- 8,991 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
// Copyright 2010 Jonas mg
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

package user

import (
	"fmt"
	"os"
	"path"
	"reflect"
	"strconv"
	"strings"
)

type userField int

// Field names for user database.
const (
	U_NAME userField = 1 << iota
	U_PASSWD
	U_UID
	U_GID
	U_GECOS
	U_DIR
	U_SHELL

	U_ALL // To get lines without searching into a field.
)

func (f userField) String() string {
	switch f {
	case U_NAME:
		return "Name"
	case U_PASSWD:
		return "Passwd"
	case U_UID:
		return "UID"
	case U_GID:
		return "GID"
	case U_GECOS:
		return "GECOS"
	case U_DIR:
		return "Dir"
	case U_SHELL:
		return "Shell"
	}
	return "ALL"
}

// An User represents an user account.
type User struct {
	// Login name. (Unique)
	Name string

	// Optional hashed password
	//
	// The hashed password field may be blank, in which case no password is
	// required to authenticate as the specified login name. However, some
	// applications which read the '/etc/passwd' file may decide not to permit
	// any access at all if the password field is blank. If the password field
	// is a lower-case "x", then the encrypted password is actually stored in
	// the "shadow(5)" file instead; there must be a corresponding line in the
	// '/etc/shadow' file, or else the user account is invalid. If the password
	// field is any other string, then it will be treated as an hashed password,
	// as specified by "crypt(3)".
	password string

	// Numerical user ID. (Unique)
	UID int

	// Numerical group ID
	GID int

	// User name or comment field
	//
	// The comment field is used by various system utilities, such as "finger(1)".
	Gecos string

	// User home directory
	//
	// The home directory field provides the name of the initial working
	// directory. The login program uses this information to set the value of
	// the $HOME environmental variable.
	Dir string

	// Optional user command interpreter
	//
	// The command interpreter field provides the name of the user's command
	// language interpreter, or the name of the initial program to execute.
	// The login program uses this information to set the value of the "$SHELL"
	// environmental variable. If this field is empty, it defaults to the value
	// "/bin/sh".
	Shell string

	addSystemUser bool
}

// NewUser returns a new User with both fields "Dir" and "Shell" got from
// the system configuration.
func NewUser(name string, gid int) *User {
	loadConfig()

	return &User{
		Name:  name,
		Dir:   path.Join(config.useradd.HOME, name),
		Shell: config.useradd.SHELL,
		UID:   -1,
		GID:   gid,
	}
}

// NewSystemUser returns a new system user.
func NewSystemUser(name, homeDir string, gid int) *User {
	return &User{
		Name:  name,
		Dir:   homeDir,
		Shell: "/bin/false",
		UID:   -1,
		GID:   gid,

		addSystemUser: true,
	}
}

func (u *User) filename() string { return fileUser }

// IsOfSystem indicates whether it is a system user.
func (u *User) IsOfSystem() bool {
	//loadConfig()

	if u.UID > config.login.SYS_UID_MIN && u.UID < config.login.SYS_UID_MAX {
		return true
	}
	return false
}

func (u *User) String() string {
	return fmt.Sprintf("%s:%s:%d:%d:%s:%s:%s\n",
		u.Name, u.password, u.UID, u.GID, u.Gecos, u.Dir, u.Shell)
}

// parseUser parses the row of an user.
func parseUser(row string) (*User, error) {
	fields := strings.Split(row, ":")
	if len(fields) != 7 {
		return nil, rowError{fileUser, row}
	}

	uid, err := strconv.Atoi(fields[2])
	if err != nil {
		return nil, atoiError{fileUser, row, "UID"}
	}
	gid, err := strconv.Atoi(fields[3])
	if err != nil {
		return nil, atoiError{fileUser, row, "GID"}
	}

	return &User{
		Name:     fields[0],
		password: fields[1],
		UID:      uid,
		GID:      gid,
		Gecos:    fields[4],
		Dir:      fields[5],
		Shell:    fields[6],
	}, nil
}

// == Lookup
//

// lookUp parses the user line searching a value into the field.
// Returns nil if is not found.
func (*User) lookUp(line string, f field, value interface{}) interface{} {
	_field := f.(userField)
	allField := strings.Split(line, ":")
	intField := make(map[int]int)

	// Check integers
	var err error
	if intField[2], err = strconv.Atoi(allField[2]); err != nil {
		panic(atoiError{fileUser, line, "UID"})
	}
	if intField[3], err = strconv.Atoi(allField[3]); err != nil {
		panic(atoiError{fileUser, line, "GID"})
	}

	// Check fields
	var isField bool
	if U_NAME&_field != 0 && allField[0] == value.(string) {
		isField = true
	} else if U_PASSWD&_field != 0 && allField[1] == value.(string) {
		isField = true
	} else if U_UID&_field != 0 && intField[2] == value.(int) {
		isField = true
	} else if U_GID&_field != 0 && intField[3] == value.(int) {
		isField = true
	} else if U_GECOS&_field != 0 && allField[4] == value.(string) {
		isField = true
	} else if U_DIR&_field != 0 && allField[5] == value.(string) {
		isField = true
	} else if U_SHELL&_field != 0 && allField[6] == value.(string) {
		isField = true
	} else if U_ALL&_field != 0 {
		isField = true
	}

	if isField {
		return &User{
			Name:     allField[0],
			password: allField[1],
			UID:      intField[2],
			GID:      intField[3],
			Gecos:    allField[4],
			Dir:      allField[5],
			Shell:    allField[6],
		}
	}
	return nil
}

// LookupUID looks up an user by user ID.
func LookupUID(uid int) (*User, error) {
	entries, err := LookupInUser(U_UID, uid, 1)
	if err != nil {
		return nil, err
	}

	return entries[0], err
}

// LookupUser looks up an user by name.
func LookupUser(name string) (*User, error) {
	entries, err := LookupInUser(U_NAME, name, 1)
	if err != nil {
		return nil, err
	}

	return entries[0], err
}

// LookupInUser looks up an user by the given values.
//
// The count determines the number of fields to return:
//   n > 0: at most n fields
//   n == 0: the result is nil (zero fields)
//   n < 0: all fields
func LookupInUser(field userField, value interface{}, n int) ([]*User, error) {
	iEntries, err := lookUp(&User{}, field, value, n)
	if err != nil {
		return nil, err
	}

	// == Convert to type user
	valueSlice := reflect.ValueOf(iEntries)
	entries := make([]*User, valueSlice.Len())

	for i := 0; i < valueSlice.Len(); i++ {
		entries[i] = valueSlice.Index(i).Interface().(*User)
	}

	return entries, err
}

// GetUsername returns the user name from the password database for the actual
// process.
// It panics whther there is an error at searching the UID.
func GetUsername() string {
	entry, err := LookupUID(os.Getuid())
	if err != nil {
		panic(err)
	}
	return entry.Name
}

// GetUsernameFromEnv returns the user name from the environment variable
// for the actual process.
func GetUsernameFromEnv() string {
	userEnv := []string{"USER", "USERNAME", "LOGNAME", "LNAME"}

	for _, val := range userEnv {
		name := os.Getenv(val)
		if name != "" {
			return name
		}
	}
	return ""
}

// == Editing
//

// AddUser adds an user to both user and shadow files.
func AddUser(name string, gid int) (uid int, err error) {
	s := NewShadow(name)
	if err = s.Add(nil); err != nil {
		return
	}

	return NewUser(name, gid).Add()
}

// AddSystemUser adds a system user to both user and shadow files.
func AddSystemUser(name, homeDir string, gid int) (uid int, err error) {
	s := NewShadow(name)
	if err = s.Add(nil); err != nil {
		return
	}

	return NewSystemUser(name, homeDir, gid).Add()
}

// Add adds a new user.
// Whether UID is < 0, it will choose the first id available in the range set
// in the system configuration.
func (u *User) Add() (uid int, err error) {
	loadConfig()

	user, err := LookupUser(u.Name)
	if err != nil {
		if _, ok := err.(NoFoundError); !ok {
			return
		}
	}
	if user != nil {
		return 0, ErrUserExist
	}

	if u.Name == "" {
		return 0, RequiredError("Name")
	}
	if u.Dir == "" {
		return 0, RequiredError("Dir")
	}
	if u.Dir == config.useradd.HOME {
		return 0, HomeError(config.useradd.HOME)
	}
	if u.Shell == "" {
		return 0, RequiredError("Shell")
	}

	var db *dbfile
	if u.UID < 0 {
		db, uid, err = nextUID(u.addSystemUser)
		if err != nil {
			db.close()
			return 0, err
		}
		u.UID = uid
	} else {
		db, err = openDBFile(fileUser, os.O_WRONLY|os.O_APPEND)
		if err != nil {
			return 0, err
		}

		// Check if Id is unique.
		_, err = LookupUID(u.UID)
		if err == nil {
			return 0, IdUsedError(u.UID)
		} else if _, ok := err.(NoFoundError); !ok {
			return 0, err
		}
	}

	u.password = "x"

	_, err = db.file.WriteString(u.String())
	err2 := db.close()
	if err2 != nil && err == nil {
		err = err2
	}
	return
}

// DelUser removes an user from the system.
func DelUser(name string) (err error) {
	err = del(name, &User{})
	if err == nil {
		err = del(name, &Shadow{})
	}
	return
}

// == Errors
//

// A HomeError reports an error at adding an account with invalid home directory.
type HomeError string

func (e HomeError) Error() string {
	return "invalid directory for the home directory of an account: " + string(e)
}