How do I manage Windows User Accounts in Go?

2020-07-20 04:36发布

问题:

I need to be able to manage Windows Local User Accounts from a Go application and it appears that without using CGo, there are no native bindings.

My initial search led me to people saying it was best to use "exec.Command" to run the "net user" command, but that seems messy and unreliable when it comes to parsing the response codes.

I've found the functions to handle this type of thing are in the netapi32.dll library, but with Go not natively supporting the Windows header files, it doesn't appear easy to call those functions.

Taking an example from https://github.com/golang/sys/tree/master/windows it appears the Go team have been redefining everything in their code then calling the DLL functions.

I'm having a hard time wrapping it together, but I've got this template of the low level API I'm aiming for, then wrapping a higher level API on top of it, much like the core Go runtime does.

type LMSTR          ????
type DWORD          ????
type LPBYTE         ????
type LPDWORD        ????
type LPWSTR         ????
type NET_API_STATUS DWORD;

type USER_INFO_1 struct {
    usri1_name              LPWSTR
    usri1_password          LPWSTR
    usri1_password_age      DWORD
    usri1_priv              DWORD
    usri1_home_dir          LPWSTR
    usri1_comment           LPWSTR
    usri1_flags             DWORD
    usri1_script_path       LPWSTR
}

type GROUP_USERS_INFO_0 struct {
    grui0_name              LPWSTR
}

type USER_INFO_1003 struct {
    usri1003_password       LPWSTR
}

const (
    USER_PRIV_GUEST         = ????
    USER_PRIV_USER          = ????
    USER_PRIV_ADMIN         = ????

    UF_SCRIPT               = ????
    UF_ACCOUNTDISABLE       = ????
    UF_HOMEDIR_REQUIRED     = ????
    UF_PASSWD_NOTREQD       = ????
    UF_PASSWD_CANT_CHANGE   = ????
    UF_LOCKOUT              = ????
    UF_DONT_EXPIRE_PASSWD   = ????
    UF_ENCRYPTED_TEXT_PASSWORD_ALLOWED = ????
    UF_NOT_DELEGATED        = ????
    UF_SMARTCARD_REQUIRED   = ????
    UF_USE_DES_KEY_ONLY     = ????
    UF_DONT_REQUIRE_PREAUTH = ????
    UF_TRUSTED_FOR_DELEGATION = ????
    UF_PASSWORD_EXPIRED     = ????
    UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION = ????

    UF_NORMAL_ACCOUNT       = ????
    UF_TEMP_DUPLICATE_ACCOUNT = ????
    UF_WORKSTATION_TRUST_ACCOUNT = ????
    UF_SERVER_TRUST_ACCOUNT = ????
    UF_INTERDOMAIN_TRUST_ACCOUNT = ????

    NERR_Success            = ????
    NERR_InvalidComputer    = ????
    NERR_NotPrimary         = ????
    NERR_GroupExists        = ????
    NERR_UserExists         = ????
    NERR_PasswordTooShort   = ????
    NERR_UserNotFound       = ????
    NERR_BufTooSmall        = ????
    NERR_InternalError      = ????
    NERR_GroupNotFound      = ????
    NERR_BadPassword        = ????
    NERR_SpeGroupOp         = ????
    NERR_LastAdmin          = ????

    ERROR_ACCESS_DENIED     = ????
    ERROR_INVALID_PASSWORD  = ????
    ERROR_INVALID_LEVEL     = ????
    ERROR_MORE_DATA         = ????
    ERROR_BAD_NETPATH       = ????
    ERROR_INVALID_NAME      = ????
    ERROR_NOT_ENOUGH_MEMORY = ????
    ERROR_INVALID_PARAMETER = ????

    FILTER_TEMP_DUPLICATE_ACCOUNT = ????
    FILTER_NORMAL_ACCOUNT   = ????
    FILTER_INTERDOMAIN_TRUST_ACCOUNT = ????
    FILTER_WORKSTATION_TRUST_ACCOUNT = ????
    FILTER_SERVER_TRUST_ACCOUNT = ????
)

func NetApiBufferFree(Buffer LPVOID) (NET_API_STATUS);

func NetUserAdd(servername LMSTR, level DWORD, buf LPBYTE, parm_err LPDWORD) (NET_API_STATUS);

func NetUserChangePassword(domainname LPCWSTR, username LPCWSTR, oldpassword LPCWSTR, newpassword LPCWSTR) (NET_API_STATUS);

func NetUserDel(servername LPCWSTR, username LPCWSTR) (NET_API_STATUS);

func NetUserEnum(servername LPCWSTR, level DWORD, filter DWORD, bufptr *LPBYTE, prefmaxlen DWORD, entriesread LPDWORD, totalentries LPDWORD, resume_handle LPDWORD) (NET_API_STATUS);

func NetUserGetGroups(servername LPCWSTR, username LPCWSTR, level DWORD, bufptr *LPBYTE, prefmaxlen DWORD, entriesread LPDWORD, totalentries LPDWORD) (NET_API_STATUS);

func NetUserSetGroups(servername LPCWSTR, username LPCWSTR, level DWORD, buf LPBYTE, num_entries DWORD) (NET_API_STATUS);

func NetUserSetInfo(servername LPCWSTR, username LPCWSTR, level DWORD, buf LPBYTE, parm_err LPDWORD) (NET_API_STATUS);

What is the best way of wrapping this together?

回答1:

Using Windows DLLs is (in my opinion) the best way to directly use the Win32 API.

If you look in the src/syscall directory of your Go installation, you can find a file called mksyscall_windows.go. This seems to be how the Go team manages all their DLL wrappers.

Use go generate to generate your code

Take a look at how syscall_windows.go uses it. Specifically it has the following go generate command:

//go:generate go run mksyscall_windows.go -output zsyscall_windows.go syscall_windows.go security_windows.go

Define the Win32 API types

They then define their types. You will need to do this yourself manually.

It is a challenge sometimes because it is vital you preserve the size and alignment of the struct fields. I use Visual Studio Community Edition to poke around at the plethora of Microsoft's defined basic types in an effort to determine their Go equivalents.

Windows uses UTF16 for strings. So you will be representing these as a *uint16. Use syscall.UTF16PtrFromString to generate one from a Go string.

Annotate Win32 API functions to export

The whole point of mksyscall_windows.go is to generate all the boilerplate code so you end up with a Go function that calls the DLL for you.

This is accomplished by adding annotations (Go comments).

For example, in syscall_windows.go you have these annotations:

//sys   GetLastError() (lasterr error)
//...
//sys   CreateHardLink(filename *uint16, existingfilename *uint16, reserved uintptr) (err error) [failretval&0xff==0] = CreateHardLinkW

mksyscall_windows.go has doc comments to help you figure out how this works. You can also look at the go-generated code in zsyscall_windows.go.

Run go generate

Its easy, just run:

go generate

Example:

For your example, create a file called win32_windows.go:

package win32

//go generate go run mksyscall_windows.go -output zwin32_windows.go win32_windows.go

type (
    LPVOID         uintptr
    LMSTR          *uint16
    DWORD          uint32
    LPBYTE         *byte
    LPDWORD        *uint32
    LPWSTR         *uint16
    NET_API_STATUS DWORD

    USER_INFO_1 struct {
        Usri1_name         LPWSTR
        Usri1_password     LPWSTR
        Usri1_password_age DWORD
        Usri1_priv         DWORD
        Usri1_home_dir     LPWSTR
        Usri1_comment      LPWSTR
        Usri1_flags        DWORD
        Usri1_script_path  LPWSTR
    }

    GROUP_USERS_INFO_0 struct {
        Grui0_name LPWSTR
    }

    USER_INFO_1003 struct {
        Usri1003_password LPWSTR
    }
)

const (
    // from LMaccess.h

    USER_PRIV_GUEST = 0
    USER_PRIV_USER  = 1
    USER_PRIV_ADMIN = 2

    UF_SCRIPT                          = 0x0001
    UF_ACCOUNTDISABLE                  = 0x0002
    UF_HOMEDIR_REQUIRED                = 0x0008
    UF_LOCKOUT                         = 0x0010
    UF_PASSWD_NOTREQD                  = 0x0020
    UF_PASSWD_CANT_CHANGE              = 0x0040
    UF_ENCRYPTED_TEXT_PASSWORD_ALLOWED = 0x0080

    UF_TEMP_DUPLICATE_ACCOUNT    = 0x0100
    UF_NORMAL_ACCOUNT            = 0x0200
    UF_INTERDOMAIN_TRUST_ACCOUNT = 0x0800
    UF_WORKSTATION_TRUST_ACCOUNT = 0x1000
    UF_SERVER_TRUST_ACCOUNT      = 0x2000

    UF_ACCOUNT_TYPE_MASK = UF_TEMP_DUPLICATE_ACCOUNT |
        UF_NORMAL_ACCOUNT |
        UF_INTERDOMAIN_TRUST_ACCOUNT |
        UF_WORKSTATION_TRUST_ACCOUNT |
        UF_SERVER_TRUST_ACCOUNT

    UF_DONT_EXPIRE_PASSWD                     = 0x10000
    UF_MNS_LOGON_ACCOUNT                      = 0x20000
    UF_SMARTCARD_REQUIRED                     = 0x40000
    UF_TRUSTED_FOR_DELEGATION                 = 0x80000
    UF_NOT_DELEGATED                          = 0x100000
    UF_USE_DES_KEY_ONLY                       = 0x200000
    UF_DONT_REQUIRE_PREAUTH                   = 0x400000
    UF_PASSWORD_EXPIRED                       = 0x800000
    UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION = 0x1000000
    UF_NO_AUTH_DATA_REQUIRED                  = 0x2000000
    UF_PARTIAL_SECRETS_ACCOUNT                = 0x4000000
    UF_USE_AES_KEYS                           = 0x8000000

    UF_SETTABLE_BITS = UF_SCRIPT |
        UF_ACCOUNTDISABLE |
        UF_LOCKOUT |
        UF_HOMEDIR_REQUIRED |
        UF_PASSWD_NOTREQD |
        UF_PASSWD_CANT_CHANGE |
        UF_ACCOUNT_TYPE_MASK |
        UF_DONT_EXPIRE_PASSWD |
        UF_MNS_LOGON_ACCOUNT |
        UF_ENCRYPTED_TEXT_PASSWORD_ALLOWED |
        UF_SMARTCARD_REQUIRED |
        UF_TRUSTED_FOR_DELEGATION |
        UF_NOT_DELEGATED |
        UF_USE_DES_KEY_ONLY |
        UF_DONT_REQUIRE_PREAUTH |
        UF_PASSWORD_EXPIRED |
        UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION |
        UF_NO_AUTH_DATA_REQUIRED |
        UF_USE_AES_KEYS |
        UF_PARTIAL_SECRETS_ACCOUNT

    FILTER_TEMP_DUPLICATE_ACCOUNT    = (0x0001)
    FILTER_NORMAL_ACCOUNT            = (0x0002)
    FILTER_INTERDOMAIN_TRUST_ACCOUNT = (0x0008)
    FILTER_WORKSTATION_TRUST_ACCOUNT = (0x0010)
    FILTER_SERVER_TRUST_ACCOUNT      = (0x0020)

    LG_INCLUDE_INDIRECT = (0x0001)

    // etc...
)

//sys NetApiBufferFree(Buffer LPVOID) (status NET_API_STATUS) = netapi32.NetApiBufferFree
//sys NetUserAdd(servername LMSTR, level DWORD, buf LPBYTE, parm_err LPDWORD) (status NET_API_STATUS) = netapi32.NetUserAdd
//sys NetUserChangePassword(domainname LPCWSTR, username LPCWSTR, oldpassword LPCWSTR, newpassword LPCWSTR) (status NET_API_STATUS) = netapi32.NetUserChangePassword
//sys NetUserDel(servername LPCWSTR, username LPCWSTR) (status NET_API_STATUS) = netapi32.NetUserDel
//sys NetUserEnum(servername LPCWSTR, level DWORD, filter DWORD, bufptr *LPBYTE, prefmaxlen DWORD, entriesread LPDWORD, totalentries LPDWORD, resume_handle LPDWORD) (status NET_API_STATUS) = netapi32.NetUserEnum
//sys NetUserGetGroups(servername LPCWSTR, username LPCWSTR, level DWORD, bufptr *LPBYTE, prefmaxlen DWORD, entriesread LPDWORD, totalentries LPDWORD) (status NET_API_STATUS) = netapi32.NetUserGetGroups
//sys NetUserSetGroups(servername LPCWSTR, username LPCWSTR, level DWORD, buf LPBYTE, num_entries DWORD) (status NET_API_STATUS) = netapi32.NetUserSetGroups
//sys NetUserSetInfo(servername LPCWSTR, username LPCWSTR, level DWORD, buf LPBYTE, parm_err LPDWORD) (status NET_API_STATUS) = netapi32.NetUserSetInfo

After running go generate (so long as you copied mksyscall_windows.go to the same directory) you will have a file called "zwin32_windows.go" (something like this):

// MACHINE GENERATED BY 'go generate' COMMAND; DO NOT EDIT

package win32

import "unsafe"
import "syscall"

var _ unsafe.Pointer

var (
    modnetapi32 = syscall.NewLazyDLL("netapi32.dll")

    procNetApiBufferFree      = modnetapi32.NewProc("NetApiBufferFree")
    procNetUserAdd            = modnetapi32.NewProc("NetUserAdd")
    procNetUserChangePassword = modnetapi32.NewProc("NetUserChangePassword")
    procNetUserDel            = modnetapi32.NewProc("NetUserDel")
    procNetUserEnum           = modnetapi32.NewProc("NetUserEnum")
    procNetUserGetGroups      = modnetapi32.NewProc("NetUserGetGroups")
    procNetUserSetGroups      = modnetapi32.NewProc("NetUserSetGroups")
    procNetUserSetInfo        = modnetapi32.NewProc("NetUserSetInfo")
)

func NetApiBufferFree(Buffer LPVOID) (status NET_API_STATUS) {
    r0, _, _ := syscall.Syscall(procNetApiBufferFree.Addr(), 1, uintptr(Buffer), 0, 0)
    status = NET_API_STATUS(r0)
    return
}

func NetUserAdd(servername LMSTR, level DWORD, buf LPBYTE, parm_err LPDWORD) (status NET_API_STATUS) {
    r0, _, _ := syscall.Syscall6(procNetUserAdd.Addr(), 4, uintptr(servername), uintptr(level), uintptr(buf), uintptr(parm_err), 0, 0)
    status = NET_API_STATUS(r0)
    return
}

func NetUserChangePassword(domainname LPCWSTR, username LPCWSTR, oldpassword LPCWSTR, newpassword LPCWSTR) (status NET_API_STATUS) {
    r0, _, _ := syscall.Syscall6(procNetUserChangePassword.Addr(), 4, uintptr(domainname), uintptr(username), uintptr(oldpassword), uintptr(newpassword), 0, 0)
    status = NET_API_STATUS(r0)
    return
}

func NetUserDel(servername LPCWSTR, username LPCWSTR) (status NET_API_STATUS) {
    r0, _, _ := syscall.Syscall(procNetUserDel.Addr(), 2, uintptr(servername), uintptr(username), 0)
    status = NET_API_STATUS(r0)
    return
}

func NetUserEnum(servername LPCWSTR, level DWORD, filter DWORD, bufptr *LPBYTE, prefmaxlen DWORD, entriesread LPDWORD, totalentries LPDWORD, resume_handle LPDWORD) (status NET_API_STATUS) {
    r0, _, _ := syscall.Syscall9(procNetUserEnum.Addr(), 8, uintptr(servername), uintptr(level), uintptr(filter), uintptr(unsafe.Pointer(bufptr)), uintptr(prefmaxlen), uintptr(entriesread), uintptr(totalentries), uintptr(resume_handle), 0)
    status = NET_API_STATUS(r0)
    return
}

func NetUserGetGroups(servername LPCWSTR, username LPCWSTR, level DWORD, bufptr *LPBYTE, prefmaxlen DWORD, entriesread LPDWORD, totalentries LPDWORD) (status NET_API_STATUS) {
    r0, _, _ := syscall.Syscall9(procNetUserGetGroups.Addr(), 7, uintptr(servername), uintptr(username), uintptr(level), uintptr(unsafe.Pointer(bufptr)), uintptr(prefmaxlen), uintptr(entriesread), uintptr(totalentries), 0, 0)
    status = NET_API_STATUS(r0)
    return
}

func NetUserSetGroups(servername LPCWSTR, username LPCWSTR, level DWORD, buf LPBYTE, num_entries DWORD) (status NET_API_STATUS) {
    r0, _, _ := syscall.Syscall6(procNetUserSetGroups.Addr(), 5, uintptr(servername), uintptr(username), uintptr(level), uintptr(buf), uintptr(num_entries), 0)
    status = NET_API_STATUS(r0)
    return
}

func NetUserSetInfo(servername LPCWSTR, username LPCWSTR, level DWORD, buf LPBYTE, parm_err LPDWORD) (status NET_API_STATUS) {
    r0, _, _ := syscall.Syscall6(procNetUserSetInfo.Addr(), 5, uintptr(servername), uintptr(username), uintptr(level), uintptr(buf), uintptr(parm_err), 0)
    status = NET_API_STATUS(r0)
    return
}

Obviously most of the work is in translating the Win32 types to their Go equivalents.

Feel free to poke around in the syscall package - they often have already defined structs you may be interested in.

ZOMG sriously??1! 2 much work!

Its better than writing that code by hand. And no CGo required!

Disclamer: I have not tested the above code to verify it actually does what you want. Working with the Win32 API is its own barrel of fun.