mirror of
https://github.com/Luzifer/password.git
synced 2024-11-14 20:22:47 +00:00
395 lines
8.8 KiB
Go
395 lines
8.8 KiB
Go
// 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 _USER_FILE }
|
|
|
|
// 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{_USER_FILE, row}
|
|
}
|
|
|
|
uid, err := strconv.Atoi(fields[2])
|
|
if err != nil {
|
|
return nil, atoiError{_USER_FILE, row, "UID"}
|
|
}
|
|
gid, err := strconv.Atoi(fields[3])
|
|
if err != nil {
|
|
return nil, atoiError{_USER_FILE, 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{_USER_FILE, line, "UID"})
|
|
}
|
|
if intField[3], err = strconv.Atoi(allField[3]); err != nil {
|
|
panic(atoiError{_USER_FILE, 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 {
|
|
user_env := []string{"USER", "USERNAME", "LOGNAME", "LNAME"}
|
|
|
|
for _, val := range user_env {
|
|
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(_USER_FILE, 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)
|
|
}
|