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  		if errors.Is(e, windows.ERROR_NONE_MAPPED) {
   177  			return "", fmt.Errorf("%w: %w", UnknownGroupError(groupname), e)
   178  		}
   179  		return "", e
   180  	}
   181  	if !isValidGroupAccountType(t) {
   182  		return "", fmt.Errorf("lookupGroupName: should be group account type, not %d", t)
   183  	}
   184  	return sid.String()
   185  }
   186  
   187  // listGroupsForUsernameAndDomain accepts username and domain and retrieves
   188  // a SID list of the local groups where this user is a member.
   189  func listGroupsForUsernameAndDomain(username, domain string) ([]string, error) {
   190  	// Check if both the domain name and user should be used.
   191  	var query string
   192  	joined, err := isDomainJoined()
   193  	if err == nil && joined && len(domain) != 0 {
   194  		query = domain + `\` + username
   195  	} else {
   196  		query = username
   197  	}
   198  	q, err := syscall.UTF16PtrFromString(query)
   199  	if err != nil {
   200  		return nil, err
   201  	}
   202  	var p0 *byte
   203  	var entriesRead, totalEntries uint32
   204  	// https://learn.microsoft.com/en-us/windows/win32/api/lmaccess/nf-lmaccess-netusergetlocalgroups
   205  	// NetUserGetLocalGroups() would return a list of LocalGroupUserInfo0
   206  	// elements which hold the names of local groups where the user participates.
   207  	// The list does not follow any sorting order.
   208  	err = windows.NetUserGetLocalGroups(nil, q, 0, windows.LG_INCLUDE_INDIRECT, &p0, windows.MAX_PREFERRED_LENGTH, &entriesRead, &totalEntries)
   209  	if err != nil {
   210  		return nil, err
   211  	}
   212  	defer syscall.NetApiBufferFree(p0)
   213  	if entriesRead == 0 {
   214  		return nil, nil
   215  	}
   216  	entries := (*[1024]windows.LocalGroupUserInfo0)(unsafe.Pointer(p0))[:entriesRead:entriesRead]
   217  	var sids []string
   218  	for _, entry := range entries {
   219  		if entry.Name == nil {
   220  			continue
   221  		}
   222  		sid, err := lookupGroupName(windows.UTF16PtrToString(entry.Name))
   223  		if err != nil {
   224  			return nil, err
   225  		}
   226  		sids = append(sids, sid)
   227  	}
   228  	return sids, nil
   229  }
   230  
   231  func newUser(uid, gid, dir, username, domain string) (*User, error) {
   232  	domainAndUser := domain + `\` + username
   233  	name, e := lookupFullName(domain, username, domainAndUser)
   234  	if e != nil {
   235  		return nil, e
   236  	}
   237  	u := &User{
   238  		Uid:      uid,
   239  		Gid:      gid,
   240  		Username: domainAndUser,
   241  		Name:     name,
   242  		HomeDir:  dir,
   243  	}
   244  	return u, nil
   245  }
   246  
   247  var (
   248  	// unused variables (in this implementation)
   249  	// modified during test to exercise code paths in the cgo implementation.
   250  	userBuffer  = 0
   251  	groupBuffer = 0
   252  )
   253  
   254  func current() (*User, error) {
   255  	// Use runAsProcessOwner to ensure that we can access the process token
   256  	// when calling syscall.OpenCurrentProcessToken if the current thread
   257  	// is impersonating a different user. See https://go.dev/issue/68647.
   258  	var usr *User
   259  	err := runAsProcessOwner(func() error {
   260  		t, e := syscall.OpenCurrentProcessToken()
   261  		if e != nil {
   262  			return e
   263  		}
   264  		defer t.Close()
   265  		u, e := t.GetTokenUser()
   266  		if e != nil {
   267  			return e
   268  		}
   269  		pg, e := t.GetTokenPrimaryGroup()
   270  		if e != nil {
   271  			return e
   272  		}
   273  		uid, e := u.User.Sid.String()
   274  		if e != nil {
   275  			return e
   276  		}
   277  		gid, e := pg.PrimaryGroup.String()
   278  		if e != nil {
   279  			return e
   280  		}
   281  		dir, e := t.GetUserProfileDirectory()
   282  		if e != nil {
   283  			return e
   284  		}
   285  		username, e := windows.GetUserName(syscall.NameSamCompatible)
   286  		if e != nil {
   287  			return e
   288  		}
   289  		displayName, e := windows.GetUserName(syscall.NameDisplay)
   290  		if e != nil {
   291  			// Historically, the username is used as fallback
   292  			// when the display name can't be retrieved.
   293  			displayName = username
   294  		}
   295  		usr = &User{
   296  			Uid:      uid,
   297  			Gid:      gid,
   298  			Username: username,
   299  			Name:     displayName,
   300  			HomeDir:  dir,
   301  		}
   302  		return nil
   303  	})
   304  	return usr, err
   305  }
   306  
   307  // runAsProcessOwner runs f in the context of the current process owner,
   308  // that is, removing any impersonation that may be in effect before calling f,
   309  // and restoring the impersonation afterwards.
   310  func runAsProcessOwner(f func() error) error {
   311  	var impersonationRollbackErr error
   312  	runtime.LockOSThread()
   313  	defer func() {
   314  		// If impersonation failed, the thread is running with the wrong token,
   315  		// so it's better to terminate it.
   316  		// This is achieved by not calling runtime.UnlockOSThread.
   317  		if impersonationRollbackErr != nil {
   318  			println("os/user: failed to revert to previous token:", impersonationRollbackErr.Error())
   319  			runtime.Goexit()
   320  		} else {
   321  			runtime.UnlockOSThread()
   322  		}
   323  	}()
   324  	prevToken, isProcessToken, err := getCurrentToken()
   325  	if err != nil {
   326  		return fmt.Errorf("os/user: failed to get current token: %w", err)
   327  	}
   328  	defer prevToken.Close()
   329  	if !isProcessToken {
   330  		if err = windows.RevertToSelf(); err != nil {
   331  			return fmt.Errorf("os/user: failed to revert to self: %w", err)
   332  		}
   333  		defer func() {
   334  			impersonationRollbackErr = windows.ImpersonateLoggedOnUser(prevToken)
   335  		}()
   336  	}
   337  	return f()
   338  }
   339  
   340  // getCurrentToken returns the current thread token, or
   341  // the process token if the thread doesn't have a token.
   342  func getCurrentToken() (t syscall.Token, isProcessToken bool, err error) {
   343  	thread, _ := windows.GetCurrentThread()
   344  	// Need TOKEN_DUPLICATE and TOKEN_IMPERSONATE to use the token in ImpersonateLoggedOnUser.
   345  	err = windows.OpenThreadToken(thread, syscall.TOKEN_QUERY|syscall.TOKEN_DUPLICATE|syscall.TOKEN_IMPERSONATE, true, &t)
   346  	if errors.Is(err, windows.ERROR_NO_TOKEN) {
   347  		// Not impersonating, use the process token.
   348  		isProcessToken = true
   349  		t, err = syscall.OpenCurrentProcessToken()
   350  	}
   351  	return t, isProcessToken, err
   352  }
   353  
   354  // lookupUserPrimaryGroup obtains the primary group SID for a user using this method:
   355  // https://support.microsoft.com/en-us/help/297951/how-to-use-the-primarygroupid-attribute-to-find-the-primary-group-for
   356  // The method follows this formula: domainRID + "-" + primaryGroupRID
   357  func lookupUserPrimaryGroup(username, domain string) (string, error) {
   358  	// get the domain RID
   359  	sid, _, t, e := syscall.LookupSID("", domain)
   360  	if e != nil {
   361  		return "", e
   362  	}
   363  	if t != syscall.SidTypeDomain {
   364  		return "", fmt.Errorf("lookupUserPrimaryGroup: should be domain account type, not %d", t)
   365  	}
   366  	domainRID, e := sid.String()
   367  	if e != nil {
   368  		return "", e
   369  	}
   370  	// If the user has joined a domain use the RID of the default primary group
   371  	// called "Domain Users":
   372  	// https://support.microsoft.com/en-us/help/243330/well-known-security-identifiers-in-windows-operating-systems
   373  	// SID: S-1-5-21domain-513
   374  	//
   375  	// The correct way to obtain the primary group of a domain user is
   376  	// probing the user primaryGroupID attribute in the server Active Directory:
   377  	// https://learn.microsoft.com/en-us/windows/win32/adschema/a-primarygroupid
   378  	//
   379  	// Note that the primary group of domain users should not be modified
   380  	// on Windows for performance reasons, even if it's possible to do that.
   381  	// The .NET Developer's Guide to Directory Services Programming - Page 409
   382  	// 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
   383  	joined, err := isDomainJoined()
   384  	if err == nil && joined {
   385  		return domainRID + "-513", nil
   386  	}
   387  	// For non-domain users call NetUserGetInfo() with level 4, which
   388  	// in this case would not have any network overhead.
   389  	// The primary group should not change from RID 513 here either
   390  	// but the group will be called "None" instead:
   391  	// https://www.adampalmer.me/iodigitalsec/2013/08/10/windows-null-session-enumeration/
   392  	// "Group 'None' (RID: 513)"
   393  	u, e := syscall.UTF16PtrFromString(username)
   394  	if e != nil {
   395  		return "", e
   396  	}
   397  	d, e := syscall.UTF16PtrFromString(domain)
   398  	if e != nil {
   399  		return "", e
   400  	}
   401  	var p *byte
   402  	e = syscall.NetUserGetInfo(d, u, 4, &p)
   403  	if e != nil {
   404  		return "", e
   405  	}
   406  	defer syscall.NetApiBufferFree(p)
   407  	i := (*windows.UserInfo4)(unsafe.Pointer(p))
   408  	return fmt.Sprintf("%s-%d", domainRID, i.PrimaryGroupID), nil
   409  }
   410  
   411  func newUserFromSid(usid *syscall.SID) (*User, error) {
   412  	username, domain, sidType, e := lookupUsernameAndDomain(usid)
   413  	if e != nil {
   414  		return nil, e
   415  	}
   416  	uid, e := usid.String()
   417  	if e != nil {
   418  		return nil, e
   419  	}
   420  	var gid string
   421  	if sidType == syscall.SidTypeWellKnownGroup {
   422  		// The SID does not contain a domain; this function's domain variable has
   423  		// been populated with the SID's identifier authority. This happens with
   424  		// special service user accounts such as "NT AUTHORITY\LocalSystem".
   425  		// In this case, gid is the same as the user SID.
   426  		gid = uid
   427  	} else {
   428  		gid, e = lookupUserPrimaryGroup(username, domain)
   429  		if e != nil {
   430  			return nil, e
   431  		}
   432  	}
   433  	// If this user has logged in at least once their home path should be stored
   434  	// in the registry under the specified SID. References:
   435  	// https://social.technet.microsoft.com/wiki/contents/articles/13895.how-to-remove-a-corrupted-user-profile-from-the-registry.aspx
   436  	// https://support.asperasoft.com/hc/en-us/articles/216127438-How-to-delete-Windows-user-profiles
   437  	//
   438  	// The registry is the most reliable way to find the home path as the user
   439  	// might have decided to move it outside of the default location,
   440  	// (e.g. C:\users). Reference:
   441  	// 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
   442  	dir, e := findHomeDirInRegistry(uid)
   443  	if e != nil {
   444  		// If the home path does not exist in the registry, the user might
   445  		// have not logged in yet; fall back to using getProfilesDirectory().
   446  		// Find the username based on a SID and append that to the result of
   447  		// getProfilesDirectory(). The domain is not relevant here.
   448  		dir, e = getProfilesDirectory()
   449  		if e != nil {
   450  			return nil, e
   451  		}
   452  		dir += `\` + username
   453  	}
   454  	return newUser(uid, gid, dir, username, domain)
   455  }
   456  
   457  func lookupUser(username string) (*User, error) {
   458  	sid, _, t, e := syscall.LookupSID("", username)
   459  	if e != nil {
   460  		if errors.Is(e, windows.ERROR_NONE_MAPPED) {
   461  			return nil, fmt.Errorf("%w: %w", UnknownUserError(username), e)
   462  		}
   463  		return nil, e
   464  	}
   465  	if !isValidUserAccountType(sid, t) {
   466  		return nil, fmt.Errorf("user: should be user account type, not %d", t)
   467  	}
   468  	return newUserFromSid(sid)
   469  }
   470  
   471  func lookupUserId(uid string) (*User, error) {
   472  	sid, e := syscall.StringToSid(uid)
   473  	if e != nil {
   474  		return nil, e
   475  	}
   476  	return newUserFromSid(sid)
   477  }
   478  
   479  func lookupGroup(groupname string) (*Group, error) {
   480  	sid, err := lookupGroupName(groupname)
   481  	if err != nil {
   482  		return nil, err
   483  	}
   484  	return &Group{Name: groupname, Gid: sid}, nil
   485  }
   486  
   487  func lookupGroupId(gid string) (*Group, error) {
   488  	sid, err := syscall.StringToSid(gid)
   489  	if err != nil {
   490  		return nil, err
   491  	}
   492  	groupname, _, t, err := sid.LookupAccount("")
   493  	if err != nil {
   494  		return nil, err
   495  	}
   496  	if !isValidGroupAccountType(t) {
   497  		return nil, fmt.Errorf("lookupGroupId: should be group account type, not %d", t)
   498  	}
   499  	return &Group{Name: groupname, Gid: gid}, nil
   500  }
   501  
   502  func listGroups(user *User) ([]string, error) {
   503  	var sids []string
   504  	if u, err := Current(); err == nil && u.Uid == user.Uid {
   505  		// It is faster and more reliable to get the groups
   506  		// of the current user from the current process token.
   507  		err := runAsProcessOwner(func() error {
   508  			t, err := syscall.OpenCurrentProcessToken()
   509  			if err != nil {
   510  				return err
   511  			}
   512  			defer t.Close()
   513  			groups, err := windows.GetTokenGroups(t)
   514  			if err != nil {
   515  				return err
   516  			}
   517  			for _, g := range groups.AllGroups() {
   518  				sid, err := g.Sid.String()
   519  				if err != nil {
   520  					return err
   521  				}
   522  				sids = append(sids, sid)
   523  			}
   524  			return nil
   525  		})
   526  		if err != nil {
   527  			return nil, err
   528  		}
   529  	} else {
   530  		sid, err := syscall.StringToSid(user.Uid)
   531  		if err != nil {
   532  			return nil, err
   533  		}
   534  		username, domain, _, err := lookupUsernameAndDomain(sid)
   535  		if err != nil {
   536  			return nil, err
   537  		}
   538  		sids, err = listGroupsForUsernameAndDomain(username, domain)
   539  		if err != nil {
   540  			return nil, err
   541  		}
   542  	}
   543  	// Add the primary group of the user to the list if it is not already there.
   544  	// This is done only to comply with the POSIX concept of a primary group.
   545  	for _, sid := range sids {
   546  		if sid == user.Gid {
   547  			return sids, nil
   548  		}
   549  	}
   550  	return append(sids, user.Gid), nil
   551  }
   552  

View as plain text