blob: faaddd2341f16ea66519c67fbd8e9b62022cb10a [file] [log] [blame]
// Copyright 2012 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package user
import (
"fmt"
"internal/syscall/windows"
"internal/syscall/windows/registry"
"syscall"
"unsafe"
)
func isDomainJoined() (bool, error) {
var domain *uint16
var status uint32
err := syscall.NetGetJoinInformation(nil, &domain, &status)
if err != nil {
return false, err
}
syscall.NetApiBufferFree((*byte)(unsafe.Pointer(domain)))
return status == syscall.NetSetupDomainName, nil
}
func lookupFullNameDomain(domainAndUser string) (string, error) {
return syscall.TranslateAccountName(domainAndUser,
syscall.NameSamCompatible, syscall.NameDisplay, 50)
}
func lookupFullNameServer(servername, username string) (string, error) {
s, e := syscall.UTF16PtrFromString(servername)
if e != nil {
return "", e
}
u, e := syscall.UTF16PtrFromString(username)
if e != nil {
return "", e
}
var p *byte
e = syscall.NetUserGetInfo(s, u, 10, &p)
if e != nil {
return "", e
}
defer syscall.NetApiBufferFree(p)
i := (*syscall.UserInfo10)(unsafe.Pointer(p))
return windows.UTF16PtrToString(i.FullName, 1024), nil
}
func lookupFullName(domain, username, domainAndUser string) (string, error) {
joined, err := isDomainJoined()
if err == nil && joined {
name, err := lookupFullNameDomain(domainAndUser)
if err == nil {
return name, nil
}
}
name, err := lookupFullNameServer(domain, username)
if err == nil {
return name, nil
}
// domain worked neither as a domain nor as a server
// could be domain server unavailable
// pretend username is fullname
return username, nil
}
// getProfilesDirectory retrieves the path to the root directory
// where user profiles are stored.
func getProfilesDirectory() (string, error) {
n := uint32(100)
for {
b := make([]uint16, n)
e := windows.GetProfilesDirectory(&b[0], &n)
if e == nil {
return syscall.UTF16ToString(b), nil
}
if e != syscall.ERROR_INSUFFICIENT_BUFFER {
return "", e
}
if n <= uint32(len(b)) {
return "", e
}
}
}
// lookupUsernameAndDomain obtains the username and domain for usid.
func lookupUsernameAndDomain(usid *syscall.SID) (username, domain string, e error) {
username, domain, t, e := usid.LookupAccount("")
if e != nil {
return "", "", e
}
if t != syscall.SidTypeUser {
return "", "", fmt.Errorf("user: should be user account type, not %d", t)
}
return username, domain, nil
}
// findHomeDirInRegistry finds the user home path based on the uid.
func findHomeDirInRegistry(uid string) (dir string, e error) {
k, e := registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList\`+uid, registry.QUERY_VALUE)
if e != nil {
return "", e
}
defer k.Close()
dir, _, e = k.GetStringValue("ProfileImagePath")
if e != nil {
return "", e
}
return dir, nil
}
// lookupGroupName accepts the name of a group and retrieves the group SID.
func lookupGroupName(groupname string) (string, error) {
sid, _, t, e := syscall.LookupSID("", groupname)
if e != nil {
return "", e
}
// https://msdn.microsoft.com/en-us/library/cc245478.aspx#gt_0387e636-5654-4910-9519-1f8326cf5ec0
// SidTypeAlias should also be treated as a group type next to SidTypeGroup
// and SidTypeWellKnownGroup:
// "alias object -> resource group: A group object..."
//
// Tests show that "Administrators" can be considered of type SidTypeAlias.
if t != syscall.SidTypeGroup && t != syscall.SidTypeWellKnownGroup && t != syscall.SidTypeAlias {
return "", fmt.Errorf("lookupGroupName: should be group account type, not %d", t)
}
return sid.String()
}
// listGroupsForUsernameAndDomain accepts username and domain and retrieves
// a SID list of the local groups where this user is a member.
func listGroupsForUsernameAndDomain(username, domain string) ([]string, error) {
// Check if both the domain name and user should be used.
var query string
joined, err := isDomainJoined()
if err == nil && joined && len(domain) != 0 {
query = domain + `\` + username
} else {
query = username
}
q, err := syscall.UTF16PtrFromString(query)
if err != nil {
return nil, err
}
var p0 *byte
var entriesRead, totalEntries uint32
// https://msdn.microsoft.com/en-us/library/windows/desktop/aa370655(v=vs.85).aspx
// NetUserGetLocalGroups() would return a list of LocalGroupUserInfo0
// elements which hold the names of local groups where the user participates.
// The list does not follow any sorting order.
//
// If no groups can be found for this user, NetUserGetLocalGroups() should
// always return the SID of a single group called "None", which
// also happens to be the primary group for the local user.
err = windows.NetUserGetLocalGroups(nil, q, 0, windows.LG_INCLUDE_INDIRECT, &p0, windows.MAX_PREFERRED_LENGTH, &entriesRead, &totalEntries)
if err != nil {
return nil, err
}
defer syscall.NetApiBufferFree(p0)
if entriesRead == 0 {
return nil, fmt.Errorf("listGroupsForUsernameAndDomain: NetUserGetLocalGroups() returned an empty list for domain: %s, username: %s", domain, username)
}
entries := (*[1024]windows.LocalGroupUserInfo0)(unsafe.Pointer(p0))[:entriesRead:entriesRead]
var sids []string
for _, entry := range entries {
if entry.Name == nil {
continue
}
sid, err := lookupGroupName(windows.UTF16PtrToString(entry.Name, 1024))
if err != nil {
return nil, err
}
sids = append(sids, sid)
}
return sids, nil
}
func newUser(uid, gid, dir, username, domain string) (*User, error) {
domainAndUser := domain + `\` + username
name, e := lookupFullName(domain, username, domainAndUser)
if e != nil {
return nil, e
}
u := &User{
Uid: uid,
Gid: gid,
Username: domainAndUser,
Name: name,
HomeDir: dir,
}
return u, nil
}
func current() (*User, error) {
t, e := syscall.OpenCurrentProcessToken()
if e != nil {
return nil, e
}
defer t.Close()
u, e := t.GetTokenUser()
if e != nil {
return nil, e
}
pg, e := t.GetTokenPrimaryGroup()
if e != nil {
return nil, e
}
uid, e := u.User.Sid.String()
if e != nil {
return nil, e
}
gid, e := pg.PrimaryGroup.String()
if e != nil {
return nil, e
}
dir, e := t.GetUserProfileDirectory()
if e != nil {
return nil, e
}
username, domain, e := lookupUsernameAndDomain(u.User.Sid)
if e != nil {
return nil, e
}
return newUser(uid, gid, dir, username, domain)
}
// lookupUserPrimaryGroup obtains the primary group SID for a user using this method:
// https://support.microsoft.com/en-us/help/297951/how-to-use-the-primarygroupid-attribute-to-find-the-primary-group-for
// The method follows this formula: domainRID + "-" + primaryGroupRID
func lookupUserPrimaryGroup(username, domain string) (string, error) {
// get the domain RID
sid, _, t, e := syscall.LookupSID("", domain)
if e != nil {
return "", e
}
if t != syscall.SidTypeDomain {
return "", fmt.Errorf("lookupUserPrimaryGroup: should be domain account type, not %d", t)
}
domainRID, e := sid.String()
if e != nil {
return "", e
}
// If the user has joined a domain use the RID of the default primary group
// called "Domain Users":
// https://support.microsoft.com/en-us/help/243330/well-known-security-identifiers-in-windows-operating-systems
// SID: S-1-5-21domain-513
//
// The correct way to obtain the primary group of a domain user is
// probing the user primaryGroupID attribute in the server Active Directory:
// https://msdn.microsoft.com/en-us/library/ms679375(v=vs.85).aspx
//
// Note that the primary group of domain users should not be modified
// on Windows for performance reasons, even if it's possible to do that.
// The .NET Developer's Guide to Directory Services Programming - Page 409
// 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
joined, err := isDomainJoined()
if err == nil && joined {
return domainRID + "-513", nil
}
// For non-domain users call NetUserGetInfo() with level 4, which
// in this case would not have any network overhead.
// The primary group should not change from RID 513 here either
// but the group will be called "None" instead:
// https://www.adampalmer.me/iodigitalsec/2013/08/10/windows-null-session-enumeration/
// "Group 'None' (RID: 513)"
u, e := syscall.UTF16PtrFromString(username)
if e != nil {
return "", e
}
d, e := syscall.UTF16PtrFromString(domain)
if e != nil {
return "", e
}
var p *byte
e = syscall.NetUserGetInfo(d, u, 4, &p)
if e != nil {
return "", e
}
defer syscall.NetApiBufferFree(p)
i := (*windows.UserInfo4)(unsafe.Pointer(p))
return fmt.Sprintf("%s-%d", domainRID, i.PrimaryGroupID), nil
}
func newUserFromSid(usid *syscall.SID) (*User, error) {
username, domain, e := lookupUsernameAndDomain(usid)
if e != nil {
return nil, e
}
gid, e := lookupUserPrimaryGroup(username, domain)
if e != nil {
return nil, e
}
uid, e := usid.String()
if e != nil {
return nil, e
}
// If this user has logged in at least once their home path should be stored
// in the registry under the specified SID. References:
// https://social.technet.microsoft.com/wiki/contents/articles/13895.how-to-remove-a-corrupted-user-profile-from-the-registry.aspx
// https://support.asperasoft.com/hc/en-us/articles/216127438-How-to-delete-Windows-user-profiles
//
// The registry is the most reliable way to find the home path as the user
// might have decided to move it outside of the default location,
// (e.g. C:\users). Reference:
// 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
dir, e := findHomeDirInRegistry(uid)
if e != nil {
// If the home path does not exist in the registry, the user might
// have not logged in yet; fall back to using getProfilesDirectory().
// Find the username based on a SID and append that to the result of
// getProfilesDirectory(). The domain is not relevant here.
dir, e = getProfilesDirectory()
if e != nil {
return nil, e
}
dir += `\` + username
}
return newUser(uid, gid, dir, username, domain)
}
func lookupUser(username string) (*User, error) {
sid, _, t, e := syscall.LookupSID("", username)
if e != nil {
return nil, e
}
if t != syscall.SidTypeUser {
return nil, fmt.Errorf("user: should be user account type, not %d", t)
}
return newUserFromSid(sid)
}
func lookupUserId(uid string) (*User, error) {
sid, e := syscall.StringToSid(uid)
if e != nil {
return nil, e
}
return newUserFromSid(sid)
}
func lookupGroup(groupname string) (*Group, error) {
sid, err := lookupGroupName(groupname)
if err != nil {
return nil, err
}
return &Group{Name: groupname, Gid: sid}, nil
}
func lookupGroupId(gid string) (*Group, error) {
sid, err := syscall.StringToSid(gid)
if err != nil {
return nil, err
}
groupname, _, t, err := sid.LookupAccount("")
if err != nil {
return nil, err
}
if t != syscall.SidTypeGroup && t != syscall.SidTypeWellKnownGroup && t != syscall.SidTypeAlias {
return nil, fmt.Errorf("lookupGroupId: should be group account type, not %d", t)
}
return &Group{Name: groupname, Gid: gid}, nil
}
func listGroups(user *User) ([]string, error) {
sid, err := syscall.StringToSid(user.Uid)
if err != nil {
return nil, err
}
username, domain, err := lookupUsernameAndDomain(sid)
if err != nil {
return nil, err
}
sids, err := listGroupsForUsernameAndDomain(username, domain)
if err != nil {
return nil, err
}
// Add the primary group of the user to the list if it is not already there.
// This is done only to comply with the POSIX concept of a primary group.
for _, sid := range sids {
if sid == user.Gid {
return sids, nil
}
}
return append(sids, user.Gid), nil
}