Source file src/os/user/lookup_windows.go

     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  

View as plain text