pkg.re/essentialkaos/ek.10@v12.41.0+incompatible/system/user.go (about)

     1  // +build linux, darwin, !windows
     2  
     3  package system
     4  
     5  // ////////////////////////////////////////////////////////////////////////////////// //
     6  //                                                                                    //
     7  //                         Copyright (c) 2022 ESSENTIAL KAOS                          //
     8  //      Apache License, Version 2.0 <https://www.apache.org/licenses/LICENSE-2.0>     //
     9  //                                                                                    //
    10  // ////////////////////////////////////////////////////////////////////////////////// //
    11  
    12  import (
    13  	"errors"
    14  	"os"
    15  	"os/exec"
    16  	"sort"
    17  	"strconv"
    18  	"strings"
    19  	"syscall"
    20  	"time"
    21  
    22  	"pkg.re/essentialkaos/ek.v12/env"
    23  	"pkg.re/essentialkaos/ek.v12/strutil"
    24  )
    25  
    26  // ////////////////////////////////////////////////////////////////////////////////// //
    27  
    28  // User contains information about user
    29  type User struct {
    30  	UID      int      `json:"uid"`
    31  	GID      int      `json:"gid"`
    32  	Name     string   `json:"name"`
    33  	Groups   []*Group `json:"groups"`
    34  	Comment  string   `json:"comment"`
    35  	Shell    string   `json:"shell"`
    36  	HomeDir  string   `json:"home_dir"`
    37  	RealUID  int      `json:"real_uid"`
    38  	RealGID  int      `json:"real_gid"`
    39  	RealName string   `json:"real_name"`
    40  }
    41  
    42  // Group contains information about group
    43  type Group struct {
    44  	Name string `json:"name"`
    45  	GID  int    `json:"gid"`
    46  }
    47  
    48  // SessionInfo contains information about all sessions
    49  type SessionInfo struct {
    50  	User             *User     `json:"user"`
    51  	LoginTime        time.Time `json:"login_time"`
    52  	LastActivityTime time.Time `json:"last_activity_time"`
    53  }
    54  
    55  // sessionsInfo is slice with SessionInfo
    56  type sessionsInfo []*SessionInfo
    57  
    58  // ////////////////////////////////////////////////////////////////////////////////// //
    59  
    60  func (s sessionsInfo) Len() int {
    61  	return len(s)
    62  }
    63  
    64  func (s sessionsInfo) Less(i, j int) bool {
    65  	return s[i].LoginTime.Unix() < s[j].LoginTime.Unix()
    66  }
    67  
    68  func (s sessionsInfo) Swap(i, j int) {
    69  	s[i], s[j] = s[j], s[i]
    70  }
    71  
    72  // ////////////////////////////////////////////////////////////////////////////////// //
    73  
    74  // Errors
    75  var (
    76  	// ErrEmptyPath is returned if given path is empty
    77  	ErrEmptyPath = errors.New("Path is empty")
    78  
    79  	// ErrEmptyUserName is returned if given user name or uid is empty
    80  	ErrEmptyUserName = errors.New("User name/ID can't be blank")
    81  
    82  	// ErrEmptyGroupName is returned if given group name of gid is empty
    83  	ErrEmptyGroupName = errors.New("Group name/ID can't be blank")
    84  
    85  	// ErrCantParseIdOutput is returned if id command output has unsupported format
    86  	ErrCantParseIdOutput = errors.New("Can't parse id command output")
    87  
    88  	// ErrCantParseGetentOutput is returned if getent command output has unsupported format
    89  	ErrCantParseGetentOutput = errors.New("Can't parse getent command output")
    90  )
    91  
    92  // CurrentUserCachePeriod is cache period for current user info
    93  var CurrentUserCachePeriod = 5 * time.Minute
    94  
    95  // ////////////////////////////////////////////////////////////////////////////////// //
    96  
    97  // Current is user info cache
    98  var curUser *User
    99  
   100  // curUserUpdateDate is date when user data was updated
   101  var curUserUpdateDate time.Time
   102  
   103  // Path to pts dir
   104  var ptsDir = "/dev/pts"
   105  
   106  // ////////////////////////////////////////////////////////////////////////////////// //
   107  
   108  // Who returns info about all active sessions sorted by login time
   109  func Who() ([]*SessionInfo, error) {
   110  	var result []*SessionInfo
   111  
   112  	ptsList := readDir(ptsDir)
   113  
   114  	if len(ptsList) == 0 {
   115  		return result, nil
   116  	}
   117  
   118  	for _, file := range ptsList {
   119  		if file == "ptmx" {
   120  			continue
   121  		}
   122  
   123  		info, err := getSessionInfo(file)
   124  
   125  		if err != nil {
   126  			continue
   127  		}
   128  
   129  		result = append(result, info)
   130  	}
   131  
   132  	if len(result) != 0 {
   133  		sort.Sort(sessionsInfo(result))
   134  	}
   135  
   136  	return result, nil
   137  }
   138  
   139  // CurrentUser returns struct with info about current user
   140  func CurrentUser(avoidCache ...bool) (*User, error) {
   141  	if len(avoidCache) == 0 || avoidCache[0] == false {
   142  		if curUser != nil && time.Since(curUserUpdateDate) < CurrentUserCachePeriod {
   143  			return curUser, nil
   144  		}
   145  	}
   146  
   147  	username, err := getCurrentUserName()
   148  
   149  	if err != nil {
   150  		return nil, err
   151  	}
   152  
   153  	user, err := LookupUser(username)
   154  
   155  	if err != nil {
   156  		return user, err
   157  	}
   158  
   159  	if user.Name == "root" {
   160  		appendRealUserInfo(user)
   161  	}
   162  
   163  	curUser = user
   164  	curUserUpdateDate = time.Now()
   165  
   166  	return user, nil
   167  }
   168  
   169  // LookupUser searches user info by given name
   170  func LookupUser(nameOrID string) (*User, error) {
   171  	if nameOrID == "" {
   172  		return nil, ErrEmptyUserName
   173  	}
   174  
   175  	user, err := getUserInfo(nameOrID)
   176  
   177  	if err != nil {
   178  		return nil, err
   179  	}
   180  
   181  	err = appendGroupInfo(user)
   182  
   183  	if err != nil {
   184  		return nil, err
   185  	}
   186  
   187  	return user, nil
   188  }
   189  
   190  // LookupGroup searches group info by given name
   191  func LookupGroup(nameOrID string) (*Group, error) {
   192  	if nameOrID == "" {
   193  		return nil, ErrEmptyGroupName
   194  	}
   195  
   196  	return getGroupInfo(nameOrID)
   197  }
   198  
   199  // CurrentTTY returns current tty or empty string if error occurred
   200  func CurrentTTY() string {
   201  	pid := strconv.Itoa(os.Getpid())
   202  	fdLink, err := os.Readlink("/proc/" + pid + "/fd/0")
   203  
   204  	if err != nil {
   205  		return ""
   206  	}
   207  
   208  	return fdLink
   209  }
   210  
   211  // ////////////////////////////////////////////////////////////////////////////////// //
   212  
   213  // IsRoot checks if current user is root
   214  func (u *User) IsRoot() bool {
   215  	return u.UID == 0 && u.GID == 0
   216  }
   217  
   218  // IsSudo checks if it user over sudo command
   219  func (u *User) IsSudo() bool {
   220  	return u.IsRoot() && u.RealUID != 0 && u.RealGID != 0
   221  }
   222  
   223  // GroupList returns slice with user groups names
   224  func (u *User) GroupList() []string {
   225  	var result []string
   226  
   227  	for _, group := range u.Groups {
   228  		result = append(result, group.Name)
   229  	}
   230  
   231  	return result
   232  }
   233  
   234  // ////////////////////////////////////////////////////////////////////////////////// //
   235  
   236  // getCurrentUserName returns name of current user
   237  func getCurrentUserName() (string, error) {
   238  	cmd := exec.Command("id", "-un")
   239  
   240  	data, err := cmd.Output()
   241  
   242  	if err != nil {
   243  		return "", ErrCantParseIdOutput
   244  	}
   245  
   246  	username := strings.TrimRight(string(data), "\n")
   247  
   248  	return username, nil
   249  }
   250  
   251  // appendGroupInfo append info about groups
   252  func appendGroupInfo(user *User) error {
   253  	cmd := exec.Command("id", user.Name)
   254  
   255  	data, err := cmd.Output()
   256  
   257  	if err != nil {
   258  		return ErrCantParseIdOutput
   259  	}
   260  
   261  	user.Groups = extractGroupsInfo(string(data))
   262  
   263  	return nil
   264  }
   265  
   266  // appendRealUserInfo append real user info when user under sudo
   267  func appendRealUserInfo(user *User) {
   268  	username, uid, gid := getRealUserByPTY()
   269  
   270  	if username == "" {
   271  		username, uid, gid = getRealUserFromEnv()
   272  	}
   273  
   274  	user.RealName = username
   275  	user.RealUID = uid
   276  	user.RealGID = gid
   277  }
   278  
   279  // getUserInfo returns UID associated with current TTY
   280  func getTDOwnerID() (int, bool) {
   281  	tty := CurrentTTY()
   282  
   283  	if tty == "" {
   284  		return -1, false
   285  	}
   286  
   287  	ownerID, err := getOwner(tty)
   288  
   289  	return ownerID, err == nil
   290  }
   291  
   292  // getRealUserByPTY try to find info about real user from real user PTY
   293  func getRealUserByPTY() (string, int, int) {
   294  	ownerID, ok := getTDOwnerID()
   295  
   296  	if !ok {
   297  		return "", -1, -1
   298  	}
   299  
   300  	realUser, err := getUserInfo(strconv.Itoa(ownerID))
   301  
   302  	if err != nil {
   303  		return "", -1, -1
   304  	}
   305  
   306  	return realUser.Name, realUser.UID, realUser.GID
   307  }
   308  
   309  // getRealUserFromEnv try to find info about real user in environment variables
   310  func getRealUserFromEnv() (string, int, int) {
   311  	envMap := env.Get()
   312  
   313  	if envMap["SUDO_USER"] == "" || envMap["SUDO_UID"] == "" || envMap["SUDO_GID"] == "" {
   314  		return "", -1, -1
   315  	}
   316  
   317  	user := envMap["SUDO_USER"]
   318  	uid, _ := strconv.Atoi(envMap["SUDO_UID"])
   319  	gid, _ := strconv.Atoi(envMap["SUDO_GID"])
   320  
   321  	return user, uid, gid
   322  }
   323  
   324  // getOwner returns file or directory owner UID
   325  func getOwner(path string) (int, error) {
   326  	if path == "" {
   327  		return -1, ErrEmptyPath
   328  	}
   329  
   330  	var stat = &syscall.Stat_t{}
   331  
   332  	err := syscall.Stat(path, stat)
   333  
   334  	if err != nil {
   335  		return -1, err
   336  	}
   337  
   338  	return int(stat.Uid), nil
   339  }
   340  
   341  // readDir returns list of files in given directory
   342  func readDir(dir string) []string {
   343  	fd, err := syscall.Open(dir, syscall.O_CLOEXEC, 0644)
   344  
   345  	if err != nil {
   346  		return nil
   347  	}
   348  
   349  	defer syscall.Close(fd)
   350  
   351  	var size = 100
   352  	var n = -1
   353  
   354  	var nbuf int
   355  	var bufp int
   356  
   357  	var buf = make([]byte, 4096)
   358  	var names = make([]string, 0, size)
   359  
   360  	for n != 0 {
   361  		if bufp >= nbuf {
   362  			bufp = 0
   363  
   364  			var errno error
   365  
   366  			nbuf, errno = fixCount(syscall.ReadDirent(fd, buf))
   367  
   368  			if errno != nil {
   369  				return names
   370  			}
   371  
   372  			if nbuf <= 0 {
   373  				break
   374  			}
   375  		}
   376  
   377  		var nb, nc int
   378  		nb, nc, names = syscall.ParseDirent(buf[bufp:nbuf], n, names)
   379  		bufp += nb
   380  		n -= nc
   381  	}
   382  
   383  	return names
   384  }
   385  
   386  // fixCount fix count for negative values
   387  func fixCount(n int, err error) (int, error) {
   388  	if n < 0 {
   389  		n = 0
   390  	}
   391  
   392  	return n, err
   393  }
   394  
   395  // getSessionInfo find session info by pts file
   396  func getSessionInfo(pts string) (*SessionInfo, error) {
   397  	ptsFile := ptsDir + "/" + pts
   398  	uid, err := getOwner(ptsFile)
   399  
   400  	if err != nil {
   401  		return nil, err
   402  	}
   403  
   404  	user, err := getUserInfo(strconv.Itoa(uid))
   405  
   406  	if err != nil {
   407  		return nil, err
   408  	}
   409  
   410  	_, mtime, ctime, err := getTimes(ptsFile)
   411  
   412  	if err != nil {
   413  		return nil, err
   414  	}
   415  
   416  	return &SessionInfo{
   417  		User:             user,
   418  		LoginTime:        ctime,
   419  		LastActivityTime: mtime,
   420  	}, nil
   421  }
   422  
   423  // extractGroupsFromIdInfo extracts info from id command output
   424  func extractGroupsInfo(data string) []*Group {
   425  	var field int
   426  	var result []*Group
   427  
   428  	data = strings.TrimRight(data, "\n")
   429  	groupsInfo := strutil.ReadField(data, 3, false, "=")
   430  
   431  	if groupsInfo == "" {
   432  		return nil
   433  	}
   434  
   435  	for {
   436  		groupInfo := strutil.ReadField(groupsInfo, field, false, ",")
   437  
   438  		if groupInfo == "" {
   439  			break
   440  		}
   441  
   442  		group, err := parseGroupInfo(groupInfo)
   443  
   444  		if err == nil {
   445  			result = append(result, group)
   446  		}
   447  
   448  		field++
   449  	}
   450  
   451  	return result
   452  }
   453  
   454  // parseGroupInfo parse group info from 'id' command
   455  func parseGroupInfo(data string) (*Group, error) {
   456  	id := strutil.ReadField(data, 0, false, "(")
   457  	name := strutil.ReadField(data, 1, false, "(")
   458  	gid, _ := strconv.Atoi(id)
   459  
   460  	if len(name) == 0 {
   461  		group, err := LookupGroup(id)
   462  
   463  		if err != nil {
   464  			return nil, err
   465  		}
   466  
   467  		return group, nil
   468  	}
   469  
   470  	return &Group{GID: gid, Name: name[:len(name)-1]}, nil
   471  }