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  	"fmt"
     9  	"internal/syscall/windows"
    10  	"internal/syscall/windows/registry"
    11  	"syscall"
    12  	"unsafe"
    13  )
    14  
    15  func isDomainJoined() (bool, error) {
    16  	var domain *uint16
    17  	var status uint32
    18  	err := syscall.NetGetJoinInformation(nil, &domain, &status)
    19  	if err != nil {
    20  		return false, err
    21  	}
    22  	syscall.NetApiBufferFree((*byte)(unsafe.Pointer(domain)))
    23  	return status == syscall.NetSetupDomainName, nil
    24  }
    25  
    26  func lookupFullNameDomain(domainAndUser string) (string, error) {
    27  	return syscall.TranslateAccountName(domainAndUser,
    28  		syscall.NameSamCompatible, syscall.NameDisplay, 50)
    29  }
    30  
    31  func lookupFullNameServer(servername, username string) (string, error) {
    32  	s, e := syscall.UTF16PtrFromString(servername)
    33  	if e != nil {
    34  		return "", e
    35  	}
    36  	u, e := syscall.UTF16PtrFromString(username)
    37  	if e != nil {
    38  		return "", e
    39  	}
    40  	var p *byte
    41  	e = syscall.NetUserGetInfo(s, u, 10, &p)
    42  	if e != nil {
    43  		return "", e
    44  	}
    45  	defer syscall.NetApiBufferFree(p)
    46  	i := (*syscall.UserInfo10)(unsafe.Pointer(p))
    47  	return windows.UTF16PtrToString(i.FullName), nil
    48  }
    49  
    50  func lookupFullName(domain, username, domainAndUser string) (string, error) {
    51  	joined, err := isDomainJoined()
    52  	if err == nil && joined {
    53  		name, err := lookupFullNameDomain(domainAndUser)
    54  		if err == nil {
    55  			return name, nil
    56  		}
    57  	}
    58  	name, err := lookupFullNameServer(domain, username)
    59  	if err == nil {
    60  		return name, nil
    61  	}
    62  	// domain worked neither as a domain nor as a server
    63  	// could be domain server unavailable
    64  	// pretend username is fullname
    65  	return username, nil
    66  }
    67  
    68  // getProfilesDirectory retrieves the path to the root directory
    69  // where user profiles are stored.
    70  func getProfilesDirectory() (string, error) {
    71  	n := uint32(100)
    72  	for {
    73  		b := make([]uint16, n)
    74  		e := windows.GetProfilesDirectory(&b[0], &n)
    75  		if e == nil {
    76  			return syscall.UTF16ToString(b), nil
    77  		}
    78  		if e != syscall.ERROR_INSUFFICIENT_BUFFER {
    79  			return "", e
    80  		}
    81  		if n <= uint32(len(b)) {
    82  			return "", e
    83  		}
    84  	}
    85  }
    86  
    87  // lookupUsernameAndDomain obtains the username and domain for usid.
    88  func lookupUsernameAndDomain(usid *syscall.SID) (username, domain string, e error) {
    89  	username, domain, t, e := usid.LookupAccount("")
    90  	if e != nil {
    91  		return "", "", e
    92  	}
    93  	if t != syscall.SidTypeUser {
    94  		return "", "", fmt.Errorf("user: should be user account type, not %d", t)
    95  	}
    96  	return username, domain, nil
    97  }
    98  
    99  // findHomeDirInRegistry finds the user home path based on the uid.
   100  func findHomeDirInRegistry(uid string) (dir string, e error) {
   101  	k, e := registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList\`+uid, registry.QUERY_VALUE)
   102  	if e != nil {
   103  		return "", e
   104  	}
   105  	defer k.Close()
   106  	dir, _, e = k.GetStringValue("ProfileImagePath")
   107  	if e != nil {
   108  		return "", e
   109  	}
   110  	return dir, nil
   111  }
   112  
   113  // lookupGroupName accepts the name of a group and retrieves the group SID.
   114  func lookupGroupName(groupname string) (string, error) {
   115  	sid, _, t, e := syscall.LookupSID("", groupname)
   116  	if e != nil {
   117  		return "", e
   118  	}
   119  	// https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-samr/7b2aeb27-92fc-41f6-8437-deb65d950921#gt_0387e636-5654-4910-9519-1f8326cf5ec0
   120  	// SidTypeAlias should also be treated as a group type next to SidTypeGroup
   121  	// and SidTypeWellKnownGroup:
   122  	// "alias object -> resource group: A group object..."
   123  	//
   124  	// Tests show that "Administrators" can be considered of type SidTypeAlias.
   125  	if t != syscall.SidTypeGroup && t != syscall.SidTypeWellKnownGroup && t != syscall.SidTypeAlias {
   126  		return "", fmt.Errorf("lookupGroupName: should be group account type, not %d", t)
   127  	}
   128  	return sid.String()
   129  }
   130  
   131  // listGroupsForUsernameAndDomain accepts username and domain and retrieves
   132  // a SID list of the local groups where this user is a member.
   133  func listGroupsForUsernameAndDomain(username, domain string) ([]string, error) {
   134  	// Check if both the domain name and user should be used.
   135  	var query string
   136  	joined, err := isDomainJoined()
   137  	if err == nil && joined && len(domain) != 0 {
   138  		query = domain + `\` + username
   139  	} else {
   140  		query = username
   141  	}
   142  	q, err := syscall.UTF16PtrFromString(query)
   143  	if err != nil {
   144  		return nil, err
   145  	}
   146  	var p0 *byte
   147  	var entriesRead, totalEntries uint32
   148  	// https://learn.microsoft.com/en-us/windows/win32/api/lmaccess/nf-lmaccess-netusergetlocalgroups
   149  	// NetUserGetLocalGroups() would return a list of LocalGroupUserInfo0
   150  	// elements which hold the names of local groups where the user participates.
   151  	// The list does not follow any sorting order.
   152  	//
   153  	// If no groups can be found for this user, NetUserGetLocalGroups() should
   154  	// always return the SID of a single group called "None", which
   155  	// also happens to be the primary group for the local user.
   156  	err = windows.NetUserGetLocalGroups(nil, q, 0, windows.LG_INCLUDE_INDIRECT, &p0, windows.MAX_PREFERRED_LENGTH, &entriesRead, &totalEntries)
   157  	if err != nil {
   158  		return nil, err
   159  	}
   160  	defer syscall.NetApiBufferFree(p0)
   161  	if entriesRead == 0 {
   162  		return nil, fmt.Errorf("listGroupsForUsernameAndDomain: NetUserGetLocalGroups() returned an empty list for domain: %s, username: %s", domain, username)
   163  	}
   164  	entries := (*[1024]windows.LocalGroupUserInfo0)(unsafe.Pointer(p0))[:entriesRead:entriesRead]
   165  	var sids []string
   166  	for _, entry := range entries {
   167  		if entry.Name == nil {
   168  			continue
   169  		}
   170  		sid, err := lookupGroupName(windows.UTF16PtrToString(entry.Name))
   171  		if err != nil {
   172  			return nil, err
   173  		}
   174  		sids = append(sids, sid)
   175  	}
   176  	return sids, nil
   177  }
   178  
   179  func newUser(uid, gid, dir, username, domain string) (*User, error) {
   180  	domainAndUser := domain + `\` + username
   181  	name, e := lookupFullName(domain, username, domainAndUser)
   182  	if e != nil {
   183  		return nil, e
   184  	}
   185  	u := &User{
   186  		Uid:      uid,
   187  		Gid:      gid,
   188  		Username: domainAndUser,
   189  		Name:     name,
   190  		HomeDir:  dir,
   191  	}
   192  	return u, nil
   193  }
   194  
   195  var (
   196  	// unused variables (in this implementation)
   197  	// modified during test to exercise code paths in the cgo implementation.
   198  	userBuffer  = 0
   199  	groupBuffer = 0
   200  )
   201  
   202  func current() (*User, error) {
   203  	t, e := syscall.OpenCurrentProcessToken()
   204  	if e != nil {
   205  		return nil, e
   206  	}
   207  	defer t.Close()
   208  	u, e := t.GetTokenUser()
   209  	if e != nil {
   210  		return nil, e
   211  	}
   212  	pg, e := t.GetTokenPrimaryGroup()
   213  	if e != nil {
   214  		return nil, e
   215  	}
   216  	uid, e := u.User.Sid.String()
   217  	if e != nil {
   218  		return nil, e
   219  	}
   220  	gid, e := pg.PrimaryGroup.String()
   221  	if e != nil {
   222  		return nil, e
   223  	}
   224  	dir, e := t.GetUserProfileDirectory()
   225  	if e != nil {
   226  		return nil, e
   227  	}
   228  	username, domain, e := lookupUsernameAndDomain(u.User.Sid)
   229  	if e != nil {
   230  		return nil, e
   231  	}
   232  	return newUser(uid, gid, dir, username, domain)
   233  }
   234  
   235  // lookupUserPrimaryGroup obtains the primary group SID for a user using this method:
   236  // https://support.microsoft.com/en-us/help/297951/how-to-use-the-primarygroupid-attribute-to-find-the-primary-group-for
   237  // The method follows this formula: domainRID + "-" + primaryGroupRID
   238  func lookupUserPrimaryGroup(username, domain string) (string, error) {
   239  	// get the domain RID
   240  	sid, _, t, e := syscall.LookupSID("", domain)
   241  	if e != nil {
   242  		return "", e
   243  	}
   244  	if t != syscall.SidTypeDomain {
   245  		return "", fmt.Errorf("lookupUserPrimaryGroup: should be domain account type, not %d", t)
   246  	}
   247  	domainRID, e := sid.String()
   248  	if e != nil {
   249  		return "", e
   250  	}
   251  	// If the user has joined a domain use the RID of the default primary group
   252  	// called "Domain Users":
   253  	// https://support.microsoft.com/en-us/help/243330/well-known-security-identifiers-in-windows-operating-systems
   254  	// SID: S-1-5-21domain-513
   255  	//
   256  	// The correct way to obtain the primary group of a domain user is
   257  	// probing the user primaryGroupID attribute in the server Active Directory:
   258  	// https://learn.microsoft.com/en-us/windows/win32/adschema/a-primarygroupid
   259  	//
   260  	// Note that the primary group of domain users should not be modified
   261  	// on Windows for performance reasons, even if it's possible to do that.
   262  	// The .NET Developer's Guide to Directory Services Programming - Page 409
   263  	// 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
   264  	joined, err := isDomainJoined()
   265  	if err == nil && joined {
   266  		return domainRID + "-513", nil
   267  	}
   268  	// For non-domain users call NetUserGetInfo() with level 4, which
   269  	// in this case would not have any network overhead.
   270  	// The primary group should not change from RID 513 here either
   271  	// but the group will be called "None" instead:
   272  	// https://www.adampalmer.me/iodigitalsec/2013/08/10/windows-null-session-enumeration/
   273  	// "Group 'None' (RID: 513)"
   274  	u, e := syscall.UTF16PtrFromString(username)
   275  	if e != nil {
   276  		return "", e
   277  	}
   278  	d, e := syscall.UTF16PtrFromString(domain)
   279  	if e != nil {
   280  		return "", e
   281  	}
   282  	var p *byte
   283  	e = syscall.NetUserGetInfo(d, u, 4, &p)
   284  	if e != nil {
   285  		return "", e
   286  	}
   287  	defer syscall.NetApiBufferFree(p)
   288  	i := (*windows.UserInfo4)(unsafe.Pointer(p))
   289  	return fmt.Sprintf("%s-%d", domainRID, i.PrimaryGroupID), nil
   290  }
   291  
   292  func newUserFromSid(usid *syscall.SID) (*User, error) {
   293  	username, domain, e := lookupUsernameAndDomain(usid)
   294  	if e != nil {
   295  		return nil, e
   296  	}
   297  	gid, e := lookupUserPrimaryGroup(username, domain)
   298  	if e != nil {
   299  		return nil, e
   300  	}
   301  	uid, e := usid.String()
   302  	if e != nil {
   303  		return nil, e
   304  	}
   305  	// If this user has logged in at least once their home path should be stored
   306  	// in the registry under the specified SID. References:
   307  	// https://social.technet.microsoft.com/wiki/contents/articles/13895.how-to-remove-a-corrupted-user-profile-from-the-registry.aspx
   308  	// https://support.asperasoft.com/hc/en-us/articles/216127438-How-to-delete-Windows-user-profiles
   309  	//
   310  	// The registry is the most reliable way to find the home path as the user
   311  	// might have decided to move it outside of the default location,
   312  	// (e.g. C:\users). Reference:
   313  	// 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
   314  	dir, e := findHomeDirInRegistry(uid)
   315  	if e != nil {
   316  		// If the home path does not exist in the registry, the user might
   317  		// have not logged in yet; fall back to using getProfilesDirectory().
   318  		// Find the username based on a SID and append that to the result of
   319  		// getProfilesDirectory(). The domain is not relevant here.
   320  		dir, e = getProfilesDirectory()
   321  		if e != nil {
   322  			return nil, e
   323  		}
   324  		dir += `\` + username
   325  	}
   326  	return newUser(uid, gid, dir, username, domain)
   327  }
   328  
   329  func lookupUser(username string) (*User, error) {
   330  	sid, _, t, e := syscall.LookupSID("", username)
   331  	if e != nil {
   332  		return nil, e
   333  	}
   334  	if t != syscall.SidTypeUser {
   335  		return nil, fmt.Errorf("user: should be user account type, not %d", t)
   336  	}
   337  	return newUserFromSid(sid)
   338  }
   339  
   340  func lookupUserId(uid string) (*User, error) {
   341  	sid, e := syscall.StringToSid(uid)
   342  	if e != nil {
   343  		return nil, e
   344  	}
   345  	return newUserFromSid(sid)
   346  }
   347  
   348  func lookupGroup(groupname string) (*Group, error) {
   349  	sid, err := lookupGroupName(groupname)
   350  	if err != nil {
   351  		return nil, err
   352  	}
   353  	return &Group{Name: groupname, Gid: sid}, nil
   354  }
   355  
   356  func lookupGroupId(gid string) (*Group, error) {
   357  	sid, err := syscall.StringToSid(gid)
   358  	if err != nil {
   359  		return nil, err
   360  	}
   361  	groupname, _, t, err := sid.LookupAccount("")
   362  	if err != nil {
   363  		return nil, err
   364  	}
   365  	if t != syscall.SidTypeGroup && t != syscall.SidTypeWellKnownGroup && t != syscall.SidTypeAlias {
   366  		return nil, fmt.Errorf("lookupGroupId: should be group account type, not %d", t)
   367  	}
   368  	return &Group{Name: groupname, Gid: gid}, nil
   369  }
   370  
   371  func listGroups(user *User) ([]string, error) {
   372  	sid, err := syscall.StringToSid(user.Uid)
   373  	if err != nil {
   374  		return nil, err
   375  	}
   376  	username, domain, err := lookupUsernameAndDomain(sid)
   377  	if err != nil {
   378  		return nil, err
   379  	}
   380  	sids, err := listGroupsForUsernameAndDomain(username, domain)
   381  	if err != nil {
   382  		return nil, err
   383  	}
   384  	// Add the primary group of the user to the list if it is not already there.
   385  	// This is done only to comply with the POSIX concept of a primary group.
   386  	for _, sid := range sids {
   387  		if sid == user.Gid {
   388  			return sids, nil
   389  		}
   390  	}
   391  	return append(sids, user.Gid), nil
   392  }
   393  

View as plain text