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 }