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  // lookupUsernameAndDomain obtains the username and domain for usid.
    90  func lookupUsernameAndDomain(usid *syscall.SID) (username, domain string, e error) {
    91  	username, domain, t, e := usid.LookupAccount("")
    92  	if e != nil {
    93  		return "", "", e
    94  	}
    95  	if t != syscall.SidTypeUser {
    96  		return "", "", fmt.Errorf("user: should be user account type, not %d", t)
    97  	}
    98  	return username, domain, nil
    99  }
   100  
   101  // findHomeDirInRegistry finds the user home path based on the uid.
   102  func findHomeDirInRegistry(uid string) (dir string, e error) {
   103  	k, e := registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList\`+uid, registry.QUERY_VALUE)
   104  	if e != nil {
   105  		return "", e
   106  	}
   107  	defer k.Close()
   108  	dir, _, e = k.GetStringValue("ProfileImagePath")
   109  	if e != nil {
   110  		return "", e
   111  	}
   112  	return dir, nil
   113  }
   114  
   115  // lookupGroupName accepts the name of a group and retrieves the group SID.
   116  func lookupGroupName(groupname string) (string, error) {
   117  	sid, _, t, e := syscall.LookupSID("", groupname)
   118  	if e != nil {
   119  		return "", e
   120  	}
   121  	// https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-samr/7b2aeb27-92fc-41f6-8437-deb65d950921#gt_0387e636-5654-4910-9519-1f8326cf5ec0
   122  	// SidTypeAlias should also be treated as a group type next to SidTypeGroup
   123  	// and SidTypeWellKnownGroup:
   124  	// "alias object -> resource group: A group object..."
   125  	//
   126  	// Tests show that "Administrators" can be considered of type SidTypeAlias.
   127  	if t != syscall.SidTypeGroup && t != syscall.SidTypeWellKnownGroup && t != syscall.SidTypeAlias {
   128  		return "", fmt.Errorf("lookupGroupName: should be group account type, not %d", t)
   129  	}
   130  	return sid.String()
   131  }
   132  
   133  // listGroupsForUsernameAndDomain accepts username and domain and retrieves
   134  // a SID list of the local groups where this user is a member.
   135  func listGroupsForUsernameAndDomain(username, domain string) ([]string, error) {
   136  	// Check if both the domain name and user should be used.
   137  	var query string
   138  	joined, err := isDomainJoined()
   139  	if err == nil && joined && len(domain) != 0 {
   140  		query = domain + `\` + username
   141  	} else {
   142  		query = username
   143  	}
   144  	q, err := syscall.UTF16PtrFromString(query)
   145  	if err != nil {
   146  		return nil, err
   147  	}
   148  	var p0 *byte
   149  	var entriesRead, totalEntries uint32
   150  	// https://learn.microsoft.com/en-us/windows/win32/api/lmaccess/nf-lmaccess-netusergetlocalgroups
   151  	// NetUserGetLocalGroups() would return a list of LocalGroupUserInfo0
   152  	// elements which hold the names of local groups where the user participates.
   153  	// The list does not follow any sorting order.
   154  	err = windows.NetUserGetLocalGroups(nil, q, 0, windows.LG_INCLUDE_INDIRECT, &p0, windows.MAX_PREFERRED_LENGTH, &entriesRead, &totalEntries)
   155  	if err != nil {
   156  		return nil, err
   157  	}
   158  	defer syscall.NetApiBufferFree(p0)
   159  	if entriesRead == 0 {
   160  		return nil, nil
   161  	}
   162  	entries := (*[1024]windows.LocalGroupUserInfo0)(unsafe.Pointer(p0))[:entriesRead:entriesRead]
   163  	var sids []string
   164  	for _, entry := range entries {
   165  		if entry.Name == nil {
   166  			continue
   167  		}
   168  		sid, err := lookupGroupName(windows.UTF16PtrToString(entry.Name))
   169  		if err != nil {
   170  			return nil, err
   171  		}
   172  		sids = append(sids, sid)
   173  	}
   174  	return sids, nil
   175  }
   176  
   177  func newUser(uid, gid, dir, username, domain string) (*User, error) {
   178  	domainAndUser := domain + `\` + username
   179  	name, e := lookupFullName(domain, username, domainAndUser)
   180  	if e != nil {
   181  		return nil, e
   182  	}
   183  	u := &User{
   184  		Uid:      uid,
   185  		Gid:      gid,
   186  		Username: domainAndUser,
   187  		Name:     name,
   188  		HomeDir:  dir,
   189  	}
   190  	return u, nil
   191  }
   192  
   193  var (
   194  	// unused variables (in this implementation)
   195  	// modified during test to exercise code paths in the cgo implementation.
   196  	userBuffer  = 0
   197  	groupBuffer = 0
   198  )
   199  
   200  func current() (*User, error) {
   201  	// Use runAsProcessOwner to ensure that we can access the process token
   202  	// when calling syscall.OpenCurrentProcessToken if the current thread
   203  	// is impersonating a different user. See https://go.dev/issue/68647.
   204  	var usr *User
   205  	err := runAsProcessOwner(func() error {
   206  		t, e := syscall.OpenCurrentProcessToken()
   207  		if e != nil {
   208  			return e
   209  		}
   210  		defer t.Close()
   211  		u, e := t.GetTokenUser()
   212  		if e != nil {
   213  			return e
   214  		}
   215  		pg, e := t.GetTokenPrimaryGroup()
   216  		if e != nil {
   217  			return e
   218  		}
   219  		uid, e := u.User.Sid.String()
   220  		if e != nil {
   221  			return e
   222  		}
   223  		gid, e := pg.PrimaryGroup.String()
   224  		if e != nil {
   225  			return e
   226  		}
   227  		dir, e := t.GetUserProfileDirectory()
   228  		if e != nil {
   229  			return e
   230  		}
   231  		username, e := windows.GetUserName(syscall.NameSamCompatible)
   232  		if e != nil {
   233  			return e
   234  		}
   235  		displayName, e := windows.GetUserName(syscall.NameDisplay)
   236  		if e != nil {
   237  			// Historically, the username is used as fallback
   238  			// when the display name can't be retrieved.
   239  			displayName = username
   240  		}
   241  		usr = &User{
   242  			Uid:      uid,
   243  			Gid:      gid,
   244  			Username: username,
   245  			Name:     displayName,
   246  			HomeDir:  dir,
   247  		}
   248  		return nil
   249  	})
   250  	return usr, err
   251  }
   252  
   253  // runAsProcessOwner runs f in the context of the current process owner,
   254  // that is, removing any impersonation that may be in effect before calling f,
   255  // and restoring the impersonation afterwards.
   256  func runAsProcessOwner(f func() error) error {
   257  	var impersonationRollbackErr error
   258  	runtime.LockOSThread()
   259  	defer func() {
   260  		// If impersonation failed, the thread is running with the wrong token,
   261  		// so it's better to terminate it.
   262  		// This is achieved by not calling runtime.UnlockOSThread.
   263  		if impersonationRollbackErr != nil {
   264  			println("os/user: failed to revert to previous token:", impersonationRollbackErr.Error())
   265  			runtime.Goexit()
   266  		} else {
   267  			runtime.UnlockOSThread()
   268  		}
   269  	}()
   270  	prevToken, isProcessToken, err := getCurrentToken()
   271  	if err != nil {
   272  		return fmt.Errorf("os/user: failed to get current token: %w", err)
   273  	}
   274  	defer prevToken.Close()
   275  	if !isProcessToken {
   276  		if err = windows.RevertToSelf(); err != nil {
   277  			return fmt.Errorf("os/user: failed to revert to self: %w", err)
   278  		}
   279  		defer func() {
   280  			impersonationRollbackErr = windows.ImpersonateLoggedOnUser(prevToken)
   281  		}()
   282  	}
   283  	return f()
   284  }
   285  
   286  // getCurrentToken returns the current thread token, or
   287  // the process token if the thread doesn't have a token.
   288  func getCurrentToken() (t syscall.Token, isProcessToken bool, err error) {
   289  	thread, _ := windows.GetCurrentThread()
   290  	// Need TOKEN_DUPLICATE and TOKEN_IMPERSONATE to use the token in ImpersonateLoggedOnUser.
   291  	err = windows.OpenThreadToken(thread, syscall.TOKEN_QUERY|syscall.TOKEN_DUPLICATE|syscall.TOKEN_IMPERSONATE, true, &t)
   292  	if errors.Is(err, windows.ERROR_NO_TOKEN) {
   293  		// Not impersonating, use the process token.
   294  		isProcessToken = true
   295  		t, err = syscall.OpenCurrentProcessToken()
   296  	}
   297  	return t, isProcessToken, err
   298  }
   299  
   300  // lookupUserPrimaryGroup obtains the primary group SID for a user using this method:
   301  // https://support.microsoft.com/en-us/help/297951/how-to-use-the-primarygroupid-attribute-to-find-the-primary-group-for
   302  // The method follows this formula: domainRID + "-" + primaryGroupRID
   303  func lookupUserPrimaryGroup(username, domain string) (string, error) {
   304  	// get the domain RID
   305  	sid, _, t, e := syscall.LookupSID("", domain)
   306  	if e != nil {
   307  		return "", e
   308  	}
   309  	if t != syscall.SidTypeDomain {
   310  		return "", fmt.Errorf("lookupUserPrimaryGroup: should be domain account type, not %d", t)
   311  	}
   312  	domainRID, e := sid.String()
   313  	if e != nil {
   314  		return "", e
   315  	}
   316  	// If the user has joined a domain use the RID of the default primary group
   317  	// called "Domain Users":
   318  	// https://support.microsoft.com/en-us/help/243330/well-known-security-identifiers-in-windows-operating-systems
   319  	// SID: S-1-5-21domain-513
   320  	//
   321  	// The correct way to obtain the primary group of a domain user is
   322  	// probing the user primaryGroupID attribute in the server Active Directory:
   323  	// https://learn.microsoft.com/en-us/windows/win32/adschema/a-primarygroupid
   324  	//
   325  	// Note that the primary group of domain users should not be modified
   326  	// on Windows for performance reasons, even if it's possible to do that.
   327  	// The .NET Developer's Guide to Directory Services Programming - Page 409
   328  	// 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
   329  	joined, err := isDomainJoined()
   330  	if err == nil && joined {
   331  		return domainRID + "-513", nil
   332  	}
   333  	// For non-domain users call NetUserGetInfo() with level 4, which
   334  	// in this case would not have any network overhead.
   335  	// The primary group should not change from RID 513 here either
   336  	// but the group will be called "None" instead:
   337  	// https://www.adampalmer.me/iodigitalsec/2013/08/10/windows-null-session-enumeration/
   338  	// "Group 'None' (RID: 513)"
   339  	u, e := syscall.UTF16PtrFromString(username)
   340  	if e != nil {
   341  		return "", e
   342  	}
   343  	d, e := syscall.UTF16PtrFromString(domain)
   344  	if e != nil {
   345  		return "", e
   346  	}
   347  	var p *byte
   348  	e = syscall.NetUserGetInfo(d, u, 4, &p)
   349  	if e != nil {
   350  		return "", e
   351  	}
   352  	defer syscall.NetApiBufferFree(p)
   353  	i := (*windows.UserInfo4)(unsafe.Pointer(p))
   354  	return fmt.Sprintf("%s-%d", domainRID, i.PrimaryGroupID), nil
   355  }
   356  
   357  func newUserFromSid(usid *syscall.SID) (*User, error) {
   358  	username, domain, e := lookupUsernameAndDomain(usid)
   359  	if e != nil {
   360  		return nil, e
   361  	}
   362  	gid, e := lookupUserPrimaryGroup(username, domain)
   363  	if e != nil {
   364  		return nil, e
   365  	}
   366  	uid, e := usid.String()
   367  	if e != nil {
   368  		return nil, e
   369  	}
   370  	// If this user has logged in at least once their home path should be stored
   371  	// in the registry under the specified SID. References:
   372  	// https://social.technet.microsoft.com/wiki/contents/articles/13895.how-to-remove-a-corrupted-user-profile-from-the-registry.aspx
   373  	// https://support.asperasoft.com/hc/en-us/articles/216127438-How-to-delete-Windows-user-profiles
   374  	//
   375  	// The registry is the most reliable way to find the home path as the user
   376  	// might have decided to move it outside of the default location,
   377  	// (e.g. C:\users). Reference:
   378  	// 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
   379  	dir, e := findHomeDirInRegistry(uid)
   380  	if e != nil {
   381  		// If the home path does not exist in the registry, the user might
   382  		// have not logged in yet; fall back to using getProfilesDirectory().
   383  		// Find the username based on a SID and append that to the result of
   384  		// getProfilesDirectory(). The domain is not relevant here.
   385  		dir, e = getProfilesDirectory()
   386  		if e != nil {
   387  			return nil, e
   388  		}
   389  		dir += `\` + username
   390  	}
   391  	return newUser(uid, gid, dir, username, domain)
   392  }
   393  
   394  func lookupUser(username string) (*User, error) {
   395  	sid, _, t, e := syscall.LookupSID("", username)
   396  	if e != nil {
   397  		return nil, e
   398  	}
   399  	if t != syscall.SidTypeUser {
   400  		return nil, fmt.Errorf("user: should be user account type, not %d", t)
   401  	}
   402  	return newUserFromSid(sid)
   403  }
   404  
   405  func lookupUserId(uid string) (*User, error) {
   406  	sid, e := syscall.StringToSid(uid)
   407  	if e != nil {
   408  		return nil, e
   409  	}
   410  	return newUserFromSid(sid)
   411  }
   412  
   413  func lookupGroup(groupname string) (*Group, error) {
   414  	sid, err := lookupGroupName(groupname)
   415  	if err != nil {
   416  		return nil, err
   417  	}
   418  	return &Group{Name: groupname, Gid: sid}, nil
   419  }
   420  
   421  func lookupGroupId(gid string) (*Group, error) {
   422  	sid, err := syscall.StringToSid(gid)
   423  	if err != nil {
   424  		return nil, err
   425  	}
   426  	groupname, _, t, err := sid.LookupAccount("")
   427  	if err != nil {
   428  		return nil, err
   429  	}
   430  	if t != syscall.SidTypeGroup && t != syscall.SidTypeWellKnownGroup && t != syscall.SidTypeAlias {
   431  		return nil, fmt.Errorf("lookupGroupId: should be group account type, not %d", t)
   432  	}
   433  	return &Group{Name: groupname, Gid: gid}, nil
   434  }
   435  
   436  func listGroups(user *User) ([]string, error) {
   437  	var sids []string
   438  	if u, err := Current(); err == nil && u.Uid == user.Uid {
   439  		// It is faster and more reliable to get the groups
   440  		// of the current user from the current process token.
   441  		err := runAsProcessOwner(func() error {
   442  			t, err := syscall.OpenCurrentProcessToken()
   443  			if err != nil {
   444  				return err
   445  			}
   446  			defer t.Close()
   447  			groups, err := windows.GetTokenGroups(t)
   448  			if err != nil {
   449  				return err
   450  			}
   451  			for _, g := range groups.AllGroups() {
   452  				sid, err := g.Sid.String()
   453  				if err != nil {
   454  					return err
   455  				}
   456  				sids = append(sids, sid)
   457  			}
   458  			return nil
   459  		})
   460  		if err != nil {
   461  			return nil, err
   462  		}
   463  	} else {
   464  		sid, err := syscall.StringToSid(user.Uid)
   465  		if err != nil {
   466  			return nil, err
   467  		}
   468  		username, domain, err := lookupUsernameAndDomain(sid)
   469  		if err != nil {
   470  			return nil, err
   471  		}
   472  		sids, err = listGroupsForUsernameAndDomain(username, domain)
   473  		if err != nil {
   474  			return nil, err
   475  		}
   476  	}
   477  	// Add the primary group of the user to the list if it is not already there.
   478  	// This is done only to comply with the POSIX concept of a primary group.
   479  	for _, sid := range sids {
   480  		if sid == user.Gid {
   481  			return sids, nil
   482  		}
   483  	}
   484  	return append(sids, user.Gid), nil
   485  }
   486  

View as plain text