lookup_windows.mx raw

   1  // Copyright 2012 The Go Authors. All rights reserved.
   2  // Use of this source code is governed by a BSD-style
   3  // license that can be found in the LICENSE file.
   4  
   5  package user
   6  
   7  import (
   8  	"errors"
   9  	"fmt"
  10  	"internal/syscall/windows"
  11  	"internal/syscall/windows/registry"
  12  	"runtime"
  13  	"syscall"
  14  	"unsafe"
  15  )
  16  
  17  func isDomainJoined() (bool, error) {
  18  	var domain *uint16
  19  	var status uint32
  20  	err := syscall.NetGetJoinInformation(nil, &domain, &status)
  21  	if err != nil {
  22  		return false, err
  23  	}
  24  	syscall.NetApiBufferFree((*byte)(unsafe.Pointer(domain)))
  25  	return status == syscall.NetSetupDomainName, nil
  26  }
  27  
  28  func lookupFullNameDomain(domainAndUser string) (string, error) {
  29  	return syscall.TranslateAccountName(domainAndUser,
  30  		syscall.NameSamCompatible, syscall.NameDisplay, 50)
  31  }
  32  
  33  func lookupFullNameServer(servername, username string) (string, error) {
  34  	s, e := syscall.UTF16PtrFromString(servername)
  35  	if e != nil {
  36  		return "", e
  37  	}
  38  	u, e := syscall.UTF16PtrFromString(username)
  39  	if e != nil {
  40  		return "", e
  41  	}
  42  	var p *byte
  43  	e = syscall.NetUserGetInfo(s, u, 10, &p)
  44  	if e != nil {
  45  		return "", e
  46  	}
  47  	defer syscall.NetApiBufferFree(p)
  48  	i := (*syscall.UserInfo10)(unsafe.Pointer(p))
  49  	return windows.UTF16PtrToString(i.FullName), nil
  50  }
  51  
  52  func lookupFullName(domain, username, domainAndUser string) (string, error) {
  53  	joined, err := isDomainJoined()
  54  	if err == nil && joined {
  55  		name, err := lookupFullNameDomain(domainAndUser)
  56  		if err == nil {
  57  			return name, nil
  58  		}
  59  	}
  60  	name, err := lookupFullNameServer(domain, username)
  61  	if err == nil {
  62  		return name, nil
  63  	}
  64  	// domain worked neither as a domain nor as a server
  65  	// could be domain server unavailable
  66  	// pretend username is fullname
  67  	return username, nil
  68  }
  69  
  70  // getProfilesDirectory retrieves the path to the root directory
  71  // where user profiles are stored.
  72  func getProfilesDirectory() (string, error) {
  73  	n := uint32(100)
  74  	for {
  75  		b := make([]uint16, n)
  76  		e := windows.GetProfilesDirectory(&b[0], &n)
  77  		if e == nil {
  78  			return syscall.UTF16ToString(b), nil
  79  		}
  80  		if e != syscall.ERROR_INSUFFICIENT_BUFFER {
  81  			return "", e
  82  		}
  83  		if n <= uint32(len(b)) {
  84  			return "", e
  85  		}
  86  	}
  87  }
  88  
  89  func isServiceAccount(sid *syscall.SID) bool {
  90  	if !windows.IsValidSid(sid) {
  91  		// We don't accept SIDs from the public API, so this should never happen.
  92  		// Better be on the safe side and validate anyway.
  93  		return false
  94  	}
  95  	// The following RIDs are considered service user accounts as per
  96  	// https://learn.microsoft.com/en-us/windows/win32/secauthz/well-known-sids and
  97  	// https://learn.microsoft.com/en-us/windows/win32/services/service-user-accounts:
  98  	// - "S-1-5-18": LocalSystem
  99  	// - "S-1-5-19": LocalService
 100  	// - "S-1-5-20": NetworkService
 101  	if windows.GetSidSubAuthorityCount(sid) != windows.SID_REVISION ||
 102  		windows.GetSidIdentifierAuthority(sid) != windows.SECURITY_NT_AUTHORITY {
 103  		return false
 104  	}
 105  	switch windows.GetSidSubAuthority(sid, 0) {
 106  	case windows.SECURITY_LOCAL_SYSTEM_RID,
 107  		windows.SECURITY_LOCAL_SERVICE_RID,
 108  		windows.SECURITY_NETWORK_SERVICE_RID:
 109  		return true
 110  	}
 111  	return false
 112  }
 113  
 114  func isValidUserAccountType(sid *syscall.SID, sidType uint32) bool {
 115  	switch sidType {
 116  	case syscall.SidTypeUser:
 117  		return true
 118  	case syscall.SidTypeWellKnownGroup:
 119  		return isServiceAccount(sid)
 120  	}
 121  	return false
 122  }
 123  
 124  func isValidGroupAccountType(sidType uint32) bool {
 125  	switch sidType {
 126  	case syscall.SidTypeGroup:
 127  		return true
 128  	case syscall.SidTypeWellKnownGroup:
 129  		// Some well-known groups are also considered service accounts,
 130  		// so isValidUserAccountType would return true for them.
 131  		// We have historically allowed them in LookupGroup and LookupGroupId,
 132  		// so don't treat them as invalid here.
 133  		return true
 134  	case syscall.SidTypeAlias:
 135  		// https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-samr/7b2aeb27-92fc-41f6-8437-deb65d950921#gt_0387e636-5654-4910-9519-1f8326cf5ec0
 136  		// SidTypeAlias should also be treated as a group type next to SidTypeGroup
 137  		// and SidTypeWellKnownGroup:
 138  		// "alias object -> resource group: A group object..."
 139  		//
 140  		// Tests show that "Administrators" can be considered of type SidTypeAlias.
 141  		return true
 142  	}
 143  	return false
 144  }
 145  
 146  // lookupUsernameAndDomain obtains the username and domain for usid.
 147  func lookupUsernameAndDomain(usid *syscall.SID) (username, domain string, sidType uint32, e error) {
 148  	username, domain, sidType, e = usid.LookupAccount("")
 149  	if e != nil {
 150  		return "", "", 0, e
 151  	}
 152  	if !isValidUserAccountType(usid, sidType) {
 153  		return "", "", 0, fmt.Errorf("user: should be user account type, not %d", sidType)
 154  	}
 155  	return username, domain, sidType, nil
 156  }
 157  
 158  // findHomeDirInRegistry finds the user home path based on the uid.
 159  func findHomeDirInRegistry(uid string) (dir string, e error) {
 160  	k, e := registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList\`+uid, registry.QUERY_VALUE)
 161  	if e != nil {
 162  		return "", e
 163  	}
 164  	defer k.Close()
 165  	dir, _, e = k.GetStringValue("ProfileImagePath")
 166  	if e != nil {
 167  		return "", e
 168  	}
 169  	return dir, nil
 170  }
 171  
 172  // lookupGroupName accepts the name of a group and retrieves the group SID.
 173  func lookupGroupName(groupname string) (string, error) {
 174  	sid, _, t, e := syscall.LookupSID("", groupname)
 175  	if e != nil {
 176  		return "", e
 177  	}
 178  	if !isValidGroupAccountType(t) {
 179  		return "", fmt.Errorf("lookupGroupName: should be group account type, not %d", t)
 180  	}
 181  	return sid.String()
 182  }
 183  
 184  // listGroupsForUsernameAndDomain accepts username and domain and retrieves
 185  // a SID list of the local groups where this user is a member.
 186  func listGroupsForUsernameAndDomain(username, domain string) ([]string, error) {
 187  	// Check if both the domain name and user should be used.
 188  	var query string
 189  	joined, err := isDomainJoined()
 190  	if err == nil && joined && len(domain) != 0 {
 191  		query = domain + `\` + username
 192  	} else {
 193  		query = username
 194  	}
 195  	q, err := syscall.UTF16PtrFromString(query)
 196  	if err != nil {
 197  		return nil, err
 198  	}
 199  	var p0 *byte
 200  	var entriesRead, totalEntries uint32
 201  	// https://learn.microsoft.com/en-us/windows/win32/api/lmaccess/nf-lmaccess-netusergetlocalgroups
 202  	// NetUserGetLocalGroups() would return a list of LocalGroupUserInfo0
 203  	// elements which hold the names of local groups where the user participates.
 204  	// The list does not follow any sorting order.
 205  	err = windows.NetUserGetLocalGroups(nil, q, 0, windows.LG_INCLUDE_INDIRECT, &p0, windows.MAX_PREFERRED_LENGTH, &entriesRead, &totalEntries)
 206  	if err != nil {
 207  		return nil, err
 208  	}
 209  	defer syscall.NetApiBufferFree(p0)
 210  	if entriesRead == 0 {
 211  		return nil, nil
 212  	}
 213  	entries := (*[1024]windows.LocalGroupUserInfo0)(unsafe.Pointer(p0))[:entriesRead:entriesRead]
 214  	var sids []string
 215  	for _, entry := range entries {
 216  		if entry.Name == nil {
 217  			continue
 218  		}
 219  		sid, err := lookupGroupName(windows.UTF16PtrToString(entry.Name))
 220  		if err != nil {
 221  			return nil, err
 222  		}
 223  		sids = append(sids, sid)
 224  	}
 225  	return sids, nil
 226  }
 227  
 228  func newUser(uid, gid, dir, username, domain string) (*User, error) {
 229  	domainAndUser := domain + `\` + username
 230  	name, e := lookupFullName(domain, username, domainAndUser)
 231  	if e != nil {
 232  		return nil, e
 233  	}
 234  	u := &User{
 235  		Uid:      uid,
 236  		Gid:      gid,
 237  		Username: domainAndUser,
 238  		Name:     name,
 239  		HomeDir:  dir,
 240  	}
 241  	return u, nil
 242  }
 243  
 244  var (
 245  	// unused variables (in this implementation)
 246  	// modified during test to exercise code paths in the cgo implementation.
 247  	userBuffer  = 0
 248  	groupBuffer = 0
 249  )
 250  
 251  func current() (*User, error) {
 252  	// Use runAsProcessOwner to ensure that we can access the process token
 253  	// when calling syscall.OpenCurrentProcessToken if the current thread
 254  	// is impersonating a different user. See https://go.dev/issue/68647.
 255  	var usr *User
 256  	err := runAsProcessOwner(func() error {
 257  		t, e := syscall.OpenCurrentProcessToken()
 258  		if e != nil {
 259  			return e
 260  		}
 261  		defer t.Close()
 262  		u, e := t.GetTokenUser()
 263  		if e != nil {
 264  			return e
 265  		}
 266  		pg, e := t.GetTokenPrimaryGroup()
 267  		if e != nil {
 268  			return e
 269  		}
 270  		uid, e := u.User.Sid.String()
 271  		if e != nil {
 272  			return e
 273  		}
 274  		gid, e := pg.PrimaryGroup.String()
 275  		if e != nil {
 276  			return e
 277  		}
 278  		dir, e := t.GetUserProfileDirectory()
 279  		if e != nil {
 280  			return e
 281  		}
 282  		username, e := windows.GetUserName(syscall.NameSamCompatible)
 283  		if e != nil {
 284  			return e
 285  		}
 286  		displayName, e := windows.GetUserName(syscall.NameDisplay)
 287  		if e != nil {
 288  			// Historically, the username is used as fallback
 289  			// when the display name can't be retrieved.
 290  			displayName = username
 291  		}
 292  		usr = &User{
 293  			Uid:      uid,
 294  			Gid:      gid,
 295  			Username: username,
 296  			Name:     displayName,
 297  			HomeDir:  dir,
 298  		}
 299  		return nil
 300  	})
 301  	return usr, err
 302  }
 303  
 304  // runAsProcessOwner runs f in the context of the current process owner,
 305  // that is, removing any impersonation that may be in effect before calling f,
 306  // and restoring the impersonation afterwards.
 307  func runAsProcessOwner(f func() error) error {
 308  	var impersonationRollbackErr error
 309  	runtime.LockOSThread()
 310  	defer func() {
 311  		// If impersonation failed, the thread is running with the wrong token,
 312  		// so it's better to terminate it.
 313  		// This is achieved by not calling runtime.UnlockOSThread.
 314  		if impersonationRollbackErr != nil {
 315  			println("os/user: failed to revert to previous token:", impersonationRollbackErr.Error())
 316  			runtime.Goexit()
 317  		} else {
 318  			runtime.UnlockOSThread()
 319  		}
 320  	}()
 321  	prevToken, isProcessToken, err := getCurrentToken()
 322  	if err != nil {
 323  		return fmt.Errorf("os/user: failed to get current token: %w", err)
 324  	}
 325  	defer prevToken.Close()
 326  	if !isProcessToken {
 327  		if err = windows.RevertToSelf(); err != nil {
 328  			return fmt.Errorf("os/user: failed to revert to self: %w", err)
 329  		}
 330  		defer func() {
 331  			impersonationRollbackErr = windows.ImpersonateLoggedOnUser(prevToken)
 332  		}()
 333  	}
 334  	return f()
 335  }
 336  
 337  // getCurrentToken returns the current thread token, or
 338  // the process token if the thread doesn't have a token.
 339  func getCurrentToken() (t syscall.Token, isProcessToken bool, err error) {
 340  	thread, _ := windows.GetCurrentThread()
 341  	// Need TOKEN_DUPLICATE and TOKEN_IMPERSONATE to use the token in ImpersonateLoggedOnUser.
 342  	err = windows.OpenThreadToken(thread, syscall.TOKEN_QUERY|syscall.TOKEN_DUPLICATE|syscall.TOKEN_IMPERSONATE, true, &t)
 343  	if errors.Is(err, windows.ERROR_NO_TOKEN) {
 344  		// Not impersonating, use the process token.
 345  		isProcessToken = true
 346  		t, err = syscall.OpenCurrentProcessToken()
 347  	}
 348  	return t, isProcessToken, err
 349  }
 350  
 351  // lookupUserPrimaryGroup obtains the primary group SID for a user using this method:
 352  // https://support.microsoft.com/en-us/help/297951/how-to-use-the-primarygroupid-attribute-to-find-the-primary-group-for
 353  // The method follows this formula: domainRID + "-" + primaryGroupRID
 354  func lookupUserPrimaryGroup(username, domain string) (string, error) {
 355  	// get the domain RID
 356  	sid, _, t, e := syscall.LookupSID("", domain)
 357  	if e != nil {
 358  		return "", e
 359  	}
 360  	if t != syscall.SidTypeDomain {
 361  		return "", fmt.Errorf("lookupUserPrimaryGroup: should be domain account type, not %d", t)
 362  	}
 363  	domainRID, e := sid.String()
 364  	if e != nil {
 365  		return "", e
 366  	}
 367  	// If the user has joined a domain use the RID of the default primary group
 368  	// called "Domain Users":
 369  	// https://support.microsoft.com/en-us/help/243330/well-known-security-identifiers-in-windows-operating-systems
 370  	// SID: S-1-5-21domain-513
 371  	//
 372  	// The correct way to obtain the primary group of a domain user is
 373  	// probing the user primaryGroupID attribute in the server Active Directory:
 374  	// https://learn.microsoft.com/en-us/windows/win32/adschema/a-primarygroupid
 375  	//
 376  	// Note that the primary group of domain users should not be modified
 377  	// on Windows for performance reasons, even if it's possible to do that.
 378  	// The .NET Developer's Guide to Directory Services Programming - Page 409
 379  	// https://books.google.bg/books?id=kGApqjobEfsC&lpg=PA410&ots=p7oo-eOQL7&dq=primary%20group%20RID&hl=bg&pg=PA409#v=onepage&q&f=false
 380  	joined, err := isDomainJoined()
 381  	if err == nil && joined {
 382  		return domainRID + "-513", nil
 383  	}
 384  	// For non-domain users call NetUserGetInfo() with level 4, which
 385  	// in this case would not have any network overhead.
 386  	// The primary group should not change from RID 513 here either
 387  	// but the group will be called "None" instead:
 388  	// https://www.adampalmer.me/iodigitalsec/2013/08/10/windows-null-session-enumeration/
 389  	// "Group 'None' (RID: 513)"
 390  	u, e := syscall.UTF16PtrFromString(username)
 391  	if e != nil {
 392  		return "", e
 393  	}
 394  	d, e := syscall.UTF16PtrFromString(domain)
 395  	if e != nil {
 396  		return "", e
 397  	}
 398  	var p *byte
 399  	e = syscall.NetUserGetInfo(d, u, 4, &p)
 400  	if e != nil {
 401  		return "", e
 402  	}
 403  	defer syscall.NetApiBufferFree(p)
 404  	i := (*windows.UserInfo4)(unsafe.Pointer(p))
 405  	return fmt.Sprintf("%s-%d", domainRID, i.PrimaryGroupID), nil
 406  }
 407  
 408  func newUserFromSid(usid *syscall.SID) (*User, error) {
 409  	username, domain, sidType, e := lookupUsernameAndDomain(usid)
 410  	if e != nil {
 411  		return nil, e
 412  	}
 413  	uid, e := usid.String()
 414  	if e != nil {
 415  		return nil, e
 416  	}
 417  	var gid string
 418  	if sidType == syscall.SidTypeWellKnownGroup {
 419  		// The SID does not contain a domain; this function's domain variable has
 420  		// been populated with the SID's identifier authority. This happens with
 421  		// special service user accounts such as "NT AUTHORITY\LocalSystem".
 422  		// In this case, gid is the same as the user SID.
 423  		gid = uid
 424  	} else {
 425  		gid, e = lookupUserPrimaryGroup(username, domain)
 426  		if e != nil {
 427  			return nil, e
 428  		}
 429  	}
 430  	// If this user has logged in at least once their home path should be stored
 431  	// in the registry under the specified SID. References:
 432  	// https://social.technet.microsoft.com/wiki/contents/articles/13895.how-to-remove-a-corrupted-user-profile-from-the-registry.aspx
 433  	// https://support.asperasoft.com/hc/en-us/articles/216127438-How-to-delete-Windows-user-profiles
 434  	//
 435  	// The registry is the most reliable way to find the home path as the user
 436  	// might have decided to move it outside of the default location,
 437  	// (e.g. C:\users). Reference:
 438  	// https://answers.microsoft.com/en-us/windows/forum/windows_7-security/how-do-i-set-a-home-directory-outside-cusers-for-a/aed68262-1bf4-4a4d-93dc-7495193a440f
 439  	dir, e := findHomeDirInRegistry(uid)
 440  	if e != nil {
 441  		// If the home path does not exist in the registry, the user might
 442  		// have not logged in yet; fall back to using getProfilesDirectory().
 443  		// Find the username based on a SID and append that to the result of
 444  		// getProfilesDirectory(). The domain is not relevant here.
 445  		dir, e = getProfilesDirectory()
 446  		if e != nil {
 447  			return nil, e
 448  		}
 449  		dir += `\` + username
 450  	}
 451  	return newUser(uid, gid, dir, username, domain)
 452  }
 453  
 454  func lookupUser(username string) (*User, error) {
 455  	sid, _, t, e := syscall.LookupSID("", username)
 456  	if e != nil {
 457  		return nil, e
 458  	}
 459  	if !isValidUserAccountType(sid, t) {
 460  		return nil, fmt.Errorf("user: should be user account type, not %d", t)
 461  	}
 462  	return newUserFromSid(sid)
 463  }
 464  
 465  func lookupUserId(uid string) (*User, error) {
 466  	sid, e := syscall.StringToSid(uid)
 467  	if e != nil {
 468  		return nil, e
 469  	}
 470  	return newUserFromSid(sid)
 471  }
 472  
 473  func lookupGroup(groupname string) (*Group, error) {
 474  	sid, err := lookupGroupName(groupname)
 475  	if err != nil {
 476  		return nil, err
 477  	}
 478  	return &Group{Name: groupname, Gid: sid}, nil
 479  }
 480  
 481  func lookupGroupId(gid string) (*Group, error) {
 482  	sid, err := syscall.StringToSid(gid)
 483  	if err != nil {
 484  		return nil, err
 485  	}
 486  	groupname, _, t, err := sid.LookupAccount("")
 487  	if err != nil {
 488  		return nil, err
 489  	}
 490  	if !isValidGroupAccountType(t) {
 491  		return nil, fmt.Errorf("lookupGroupId: should be group account type, not %d", t)
 492  	}
 493  	return &Group{Name: groupname, Gid: gid}, nil
 494  }
 495  
 496  func listGroups(user *User) ([]string, error) {
 497  	var sids []string
 498  	if u, err := Current(); err == nil && u.Uid == user.Uid {
 499  		// It is faster and more reliable to get the groups
 500  		// of the current user from the current process token.
 501  		err := runAsProcessOwner(func() error {
 502  			t, err := syscall.OpenCurrentProcessToken()
 503  			if err != nil {
 504  				return err
 505  			}
 506  			defer t.Close()
 507  			groups, err := windows.GetTokenGroups(t)
 508  			if err != nil {
 509  				return err
 510  			}
 511  			for _, g := range groups.AllGroups() {
 512  				sid, err := g.Sid.String()
 513  				if err != nil {
 514  					return err
 515  				}
 516  				sids = append(sids, sid)
 517  			}
 518  			return nil
 519  		})
 520  		if err != nil {
 521  			return nil, err
 522  		}
 523  	} else {
 524  		sid, err := syscall.StringToSid(user.Uid)
 525  		if err != nil {
 526  			return nil, err
 527  		}
 528  		username, domain, _, err := lookupUsernameAndDomain(sid)
 529  		if err != nil {
 530  			return nil, err
 531  		}
 532  		sids, err = listGroupsForUsernameAndDomain(username, domain)
 533  		if err != nil {
 534  			return nil, err
 535  		}
 536  	}
 537  	// Add the primary group of the user to the list if it is not already there.
 538  	// This is done only to comply with the POSIX concept of a primary group.
 539  	for _, sid := range sids {
 540  		if sid == user.Gid {
 541  			return sids, nil
 542  		}
 543  	}
 544  	return append(sids, user.Gid), nil
 545  }
 546