]> Cypherpunks repositories - gostls13.git/commitdiff
os/user: obtain a user GID on Windows
authorLubomir I. Ivanov (VMware) <neolit123@gmail.com>
Mon, 2 Apr 2018 15:37:03 +0000 (15:37 +0000)
committerAlex Brainman <alex.brainman@gmail.com>
Wed, 4 Apr 2018 09:28:39 +0000 (09:28 +0000)
Add the following helpers in lookup_windows.go:
1) lookupGroupName() is used to obtain the SID of a group based
on name.
2) listGroupsForUsernameAndDomain() uses NetUserGetLocalGroups()
as a WINAPI backend to obtain the list of local groups for this
user.
3) lookupUserPrimaryGroup() is now used to populate the User.Gid
field when looking up a user by name.

Implement listGroups(), lookupGroupId(), lookupGroup() and no longer
return unimplemented errors.

Do not skip Windows User.Gid tests in user_test.go.

Change-Id: I81fd41b406da51f9a4cb24e50d392a333df81141
GitHub-Last-Rev: d1448fd55d6eaa0f41bf347df18b40da06791df1
GitHub-Pull-Request: golang/go#24222
Reviewed-on: https://go-review.googlesource.com/98137
Reviewed-by: Alex Brainman <alex.brainman@gmail.com>
Run-TryBot: Alex Brainman <alex.brainman@gmail.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>

src/internal/syscall/windows/security_windows.go
src/internal/syscall/windows/zsyscall_windows.go
src/os/user/lookup_windows.go
src/os/user/user_test.go

index 741ae979ed05d63d81a2e927691d2263a1cd9a48..4a2dfc0c73393fa86acf948a486ed2db50c19cd6 100644 (file)
@@ -83,3 +83,46 @@ const (
 )
 
 //sys  GetProfilesDirectory(dir *uint16, dirLen *uint32) (err error) = userenv.GetProfilesDirectoryW
+
+const (
+       LG_INCLUDE_INDIRECT  = 0x1
+       MAX_PREFERRED_LENGTH = 0xFFFFFFFF
+)
+
+type LocalGroupUserInfo0 struct {
+       Name *uint16
+}
+
+type UserInfo4 struct {
+       Name            *uint16
+       Password        *uint16
+       PasswordAge     uint32
+       Priv            uint32
+       HomeDir         *uint16
+       Comment         *uint16
+       Flags           uint32
+       ScriptPath      *uint16
+       AuthFlags       uint32
+       FullName        *uint16
+       UsrComment      *uint16
+       Parms           *uint16
+       Workstations    *uint16
+       LastLogon       uint32
+       LastLogoff      uint32
+       AcctExpires     uint32
+       MaxStorage      uint32
+       UnitsPerWeek    uint32
+       LogonHours      *byte
+       BadPwCount      uint32
+       NumLogons       uint32
+       LogonServer     *uint16
+       CountryCode     uint32
+       CodePage        uint32
+       UserSid         *syscall.SID
+       PrimaryGroupID  uint32
+       Profile         *uint16
+       HomeDirDrive    *uint16
+       PasswordExpired uint32
+}
+
+//sys  NetUserGetLocalGroups(serverName *uint16, userName *uint16, level uint32, flags uint32, buf **byte, prefMaxLen uint32, entriesRead *uint32, totalEntries *uint32) (neterr error) = netapi32.NetUserGetLocalGroups
index fb1f0442cc736514821922e4899b7039bdcdd230..296ee9c1cec11ba0208f80970d4e94f0379f369f 100644 (file)
@@ -64,6 +64,7 @@ var (
        procDuplicateTokenEx          = modadvapi32.NewProc("DuplicateTokenEx")
        procSetTokenInformation       = modadvapi32.NewProc("SetTokenInformation")
        procGetProfilesDirectoryW     = moduserenv.NewProc("GetProfilesDirectoryW")
+       procNetUserGetLocalGroups     = modnetapi32.NewProc("NetUserGetLocalGroups")
        procGetProcessMemoryInfo      = modpsapi.NewProc("GetProcessMemoryInfo")
 )
 
@@ -301,6 +302,14 @@ func GetProfilesDirectory(dir *uint16, dirLen *uint32) (err error) {
        return
 }
 
+func NetUserGetLocalGroups(serverName *uint16, userName *uint16, level uint32, flags uint32, buf **byte, prefMaxLen uint32, entriesRead *uint32, totalEntries *uint32) (neterr error) {
+       r0, _, _ := syscall.Syscall9(procNetUserGetLocalGroups.Addr(), 8, uintptr(unsafe.Pointer(serverName)), uintptr(unsafe.Pointer(userName)), uintptr(level), uintptr(flags), uintptr(unsafe.Pointer(buf)), uintptr(prefMaxLen), uintptr(unsafe.Pointer(entriesRead)), uintptr(unsafe.Pointer(totalEntries)), 0)
+       if r0 != 0 {
+               neterr = syscall.Errno(r0)
+       }
+       return
+}
+
 func GetProcessMemoryInfo(handle syscall.Handle, memCounters *PROCESS_MEMORY_COUNTERS, cb uint32) (err error) {
        r1, _, e1 := syscall.Syscall(procGetProcessMemoryInfo.Addr(), 3, uintptr(handle), uintptr(unsafe.Pointer(memCounters)), uintptr(cb))
        if r1 == 0 {
index d8ebd17d643ab792d584a8e4d2a9c6048b609038..7499f6a470ba0a9de187745085f6f575d4fe2e41 100644 (file)
@@ -5,7 +5,6 @@
 package user
 
 import (
-       "errors"
        "fmt"
        "internal/syscall/windows"
        "internal/syscall/windows/registry"
@@ -13,10 +12,6 @@ import (
        "unsafe"
 )
 
-func init() {
-       groupImplemented = false
-}
-
 func isDomainJoined() (bool, error) {
        var domain *uint16
        var status uint32
@@ -119,6 +114,73 @@ func findHomeDirInRegistry(uid string) (dir string, e error) {
        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]
+       var sids []string
+       for _, entry := range entries {
+               if entry.Name == nil {
+                       continue
+               }
+               name := syscall.UTF16ToString((*[1024]uint16)(unsafe.Pointer(entry.Name))[:])
+               sid, err := lookupGroupName(name)
+               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)
@@ -168,14 +230,72 @@ func current() (*User, error) {
        return newUser(uid, gid, dir, username, domain)
 }
 
-// TODO: The Gid field in the User struct is not set on Windows.
+// 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) {
-       gid := "unknown"
        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
@@ -224,13 +344,47 @@ func lookupUserId(uid string) (*User, error) {
 }
 
 func lookupGroup(groupname string) (*Group, error) {
-       return nil, errors.New("user: LookupGroup not implemented on windows")
+       sid, err := lookupGroupName(groupname)
+       if err != nil {
+               return nil, err
+       }
+       return &Group{Name: groupname, Gid: sid}, nil
 }
 
-func lookupGroupId(string) (*Group, error) {
-       return nil, errors.New("user: LookupGroupId not implemented on windows")
+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) ([]string, error) {
-       return nil, errors.New("user: GroupIds not implemented on windows")
+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
 }
index 72b147d0954c117dedad84c5ac6d8093c2984df0..02cd595349b5e1933335efdcb443888b5d9c9197 100644 (file)
@@ -47,10 +47,6 @@ func compare(t *testing.T, want, got *User) {
        if want.HomeDir != got.HomeDir {
                t.Errorf("got HomeDir=%q; want %q", got.HomeDir, want.HomeDir)
        }
-       // TODO: Gid is not set on Windows
-       if runtime.GOOS == "windows" {
-               t.Skip("skipping Gid comparisons")
-       }
        if want.Gid != got.Gid {
                t.Errorf("got Gid=%q; want %q", got.Gid, want.Gid)
        }