| // Copyright 2024 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 ( |
| "crypto/rand" |
| "encoding/base64" |
| "encoding/binary" |
| "errors" |
| "fmt" |
| "internal/syscall/windows" |
| "internal/testenv" |
| "os" |
| "os/exec" |
| "runtime" |
| "slices" |
| "strconv" |
| "strings" |
| "syscall" |
| "testing" |
| "unicode" |
| "unicode/utf8" |
| "unsafe" |
| ) |
| |
| // addUserAccount creates a local user account. |
| // It returns the name and password of the new account. |
| // Multiple programs or goroutines calling addUserAccount simultaneously will not choose the same directory. |
| func addUserAccount(t *testing.T) (name, password string) { |
| t.TempDir() |
| pattern := t.Name() |
| // Windows limits the user name to 20 characters, |
| // leave space for a 4 digits random suffix. |
| const maxNameLen, suffixLen = 20, 4 |
| pattern = pattern[:min(len(pattern), maxNameLen-suffixLen)] |
| // Drop unusual characters from the account name. |
| mapper := func(r rune) rune { |
| if r < utf8.RuneSelf { |
| if '0' <= r && r <= '9' || |
| 'a' <= r && r <= 'z' || |
| 'A' <= r && r <= 'Z' { |
| return r |
| } |
| } else if unicode.IsLetter(r) || unicode.IsNumber(r) { |
| return r |
| } |
| return -1 |
| } |
| pattern = strings.Map(mapper, pattern) |
| |
| // Generate a long random password. |
| var pwd [33]byte |
| rand.Read(pwd[:]) |
| // Add special chars to ensure it satisfies password requirements. |
| password = base64.StdEncoding.EncodeToString(pwd[:]) + "_-As@!%*(1)4#2" |
| password16, err := syscall.UTF16PtrFromString(password) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| try := 0 |
| for { |
| // Calculate a random suffix to append to the user name. |
| var suffix [2]byte |
| rand.Read(suffix[:]) |
| suffixStr := strconv.FormatUint(uint64(binary.LittleEndian.Uint16(suffix[:])), 10) |
| name := pattern + suffixStr[:min(len(suffixStr), suffixLen)] |
| name16, err := syscall.UTF16PtrFromString(name) |
| if err != nil { |
| t.Fatal(err) |
| } |
| // Create user. |
| userInfo := windows.UserInfo1{ |
| Name: name16, |
| Password: password16, |
| Priv: windows.USER_PRIV_USER, |
| } |
| err = windows.NetUserAdd(nil, 1, (*byte)(unsafe.Pointer(&userInfo)), nil) |
| if errors.Is(err, syscall.ERROR_ACCESS_DENIED) { |
| t.Skip("skipping test; don't have permission to create user") |
| } |
| // If the user already exists, try again with a different name. |
| if errors.Is(err, windows.NERR_UserExists) { |
| if try++; try < 1000 { |
| t.Log("user already exists, trying again with a different name") |
| continue |
| } |
| } |
| if err != nil { |
| t.Fatalf("NetUserAdd failed: %v", err) |
| } |
| // Delete the user when the test is done. |
| t.Cleanup(func() { |
| if err := windows.NetUserDel(nil, name16); err != nil { |
| if !errors.Is(err, windows.NERR_UserNotFound) { |
| t.Fatal(err) |
| } |
| } |
| }) |
| return name, password |
| } |
| } |
| |
| // windowsTestAccount creates a test user and returns a token for that user. |
| // If the user already exists, it will be deleted and recreated. |
| // The caller is responsible for closing the token. |
| func windowsTestAccount(t *testing.T) (syscall.Token, *User) { |
| if testenv.Builder() == "" { |
| // Adding and deleting users requires special permissions. |
| // Even if we have them, we don't want to create users on |
| // on dev machines, as they may not be cleaned up. |
| // See https://dev.go/issue/70396. |
| t.Skip("skipping non-hermetic test outside of Go builders") |
| } |
| name, password := addUserAccount(t) |
| name16, err := syscall.UTF16PtrFromString(name) |
| if err != nil { |
| t.Fatal(err) |
| } |
| pwd16, err := syscall.UTF16PtrFromString(password) |
| if err != nil { |
| t.Fatal(err) |
| } |
| domain, err := syscall.UTF16PtrFromString(".") |
| if err != nil { |
| t.Fatal(err) |
| } |
| const LOGON32_PROVIDER_DEFAULT = 0 |
| const LOGON32_LOGON_INTERACTIVE = 2 |
| var token syscall.Token |
| if err = windows.LogonUser(name16, domain, pwd16, LOGON32_LOGON_INTERACTIVE, LOGON32_PROVIDER_DEFAULT, &token); err != nil { |
| t.Fatal(err) |
| } |
| t.Cleanup(func() { |
| token.Close() |
| }) |
| usr, err := Lookup(name) |
| if err != nil { |
| t.Fatal(err) |
| } |
| return token, usr |
| } |
| |
| func TestImpersonatedSelf(t *testing.T) { |
| runtime.LockOSThread() |
| defer runtime.UnlockOSThread() |
| |
| want, err := current() |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| levels := []uint32{ |
| windows.SecurityAnonymous, |
| windows.SecurityIdentification, |
| windows.SecurityImpersonation, |
| windows.SecurityDelegation, |
| } |
| for _, level := range levels { |
| t.Run(strconv.Itoa(int(level)), func(t *testing.T) { |
| if err = windows.ImpersonateSelf(level); err != nil { |
| t.Fatal(err) |
| } |
| defer windows.RevertToSelf() |
| |
| got, err := current() |
| if level == windows.SecurityAnonymous { |
| // We can't get the process token when using an anonymous token, |
| // so we expect an error here. |
| if err == nil { |
| t.Fatal("expected error") |
| } |
| return |
| } |
| if err != nil { |
| t.Fatal(err) |
| } |
| compare(t, want, got) |
| }) |
| } |
| } |
| |
| func TestImpersonated(t *testing.T) { |
| runtime.LockOSThread() |
| defer runtime.UnlockOSThread() |
| |
| want, err := current() |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| // Create a test user and log in as that user. |
| token, _ := windowsTestAccount(t) |
| |
| // Impersonate the test user. |
| if err = windows.ImpersonateLoggedOnUser(token); err != nil { |
| t.Fatal(err) |
| } |
| defer func() { |
| err = windows.RevertToSelf() |
| if err != nil { |
| // If we can't revert to self, we can't continue testing. |
| panic(err) |
| } |
| }() |
| |
| got, err := current() |
| if err != nil { |
| t.Fatal(err) |
| } |
| compare(t, want, got) |
| } |
| |
| func TestCurrentNetapi32(t *testing.T) { |
| if os.Getenv("GO_WANT_HELPER_PROCESS") == "1" { |
| // Test that Current does not load netapi32.dll. |
| // First call Current. |
| Current() |
| |
| // Then check if netapi32.dll is loaded. |
| netapi32, err := syscall.UTF16PtrFromString("netapi32.dll") |
| if err != nil { |
| fmt.Fprintf(os.Stderr, "error: %s\n", err.Error()) |
| os.Exit(9) |
| return |
| } |
| mod, _ := windows.GetModuleHandle(netapi32) |
| if mod != 0 { |
| fmt.Fprintf(os.Stderr, "netapi32.dll is loaded\n") |
| os.Exit(9) |
| return |
| } |
| os.Exit(0) |
| return |
| } |
| exe := testenv.Executable(t) |
| cmd := testenv.CleanCmdEnv(exec.Command(exe, "-test.run=^TestCurrentNetapi32$")) |
| cmd.Env = append(cmd.Env, "GO_WANT_HELPER_PROCESS=1") |
| out, err := cmd.CombinedOutput() |
| if err != nil { |
| t.Fatalf("%v\n%s", err, out) |
| } |
| } |
| |
| func TestGroupIdsTestUser(t *testing.T) { |
| // Create a test user and log in as that user. |
| _, user := windowsTestAccount(t) |
| |
| gids, err := user.GroupIds() |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| if err != nil { |
| t.Fatalf("%+v.GroupIds(): %v", user, err) |
| } |
| if !slices.Contains(gids, user.Gid) { |
| t.Errorf("%+v.GroupIds() = %v; does not contain user GID %s", user, gids, user.Gid) |
| } |
| } |
| |
| var serviceAccounts = []struct { |
| sid string |
| name string |
| }{ |
| {"S-1-5-18", "NT AUTHORITY\\SYSTEM"}, |
| {"S-1-5-19", "NT AUTHORITY\\LOCAL SERVICE"}, |
| {"S-1-5-20", "NT AUTHORITY\\NETWORK SERVICE"}, |
| } |
| |
| func TestLookupServiceAccount(t *testing.T) { |
| t.Parallel() |
| for _, tt := range serviceAccounts { |
| u, err := Lookup(tt.name) |
| if err != nil { |
| t.Errorf("Lookup(%q): %v", tt.name, err) |
| continue |
| } |
| if u.Uid != tt.sid { |
| t.Errorf("unexpected uid for %q; got %q, want %q", u.Name, u.Uid, tt.sid) |
| } |
| } |
| } |
| |
| func TestLookupIdServiceAccount(t *testing.T) { |
| t.Parallel() |
| for _, tt := range serviceAccounts { |
| u, err := LookupId(tt.sid) |
| if err != nil { |
| t.Errorf("LookupId(%q): %v", tt.sid, err) |
| continue |
| } |
| if u.Gid != tt.sid { |
| t.Errorf("unexpected gid for %q; got %q, want %q", u.Name, u.Gid, tt.sid) |
| } |
| if u.Username != tt.name { |
| t.Errorf("unexpected user name for %q; got %q, want %q", u.Gid, u.Username, tt.name) |
| } |
| } |
| } |
| |
| func TestLookupGroupServiceAccount(t *testing.T) { |
| t.Parallel() |
| for _, tt := range serviceAccounts { |
| u, err := LookupGroup(tt.name) |
| if err != nil { |
| t.Errorf("LookupGroup(%q): %v", tt.name, err) |
| continue |
| } |
| if u.Gid != tt.sid { |
| t.Errorf("unexpected gid for %q; got %q, want %q", u.Name, u.Gid, tt.sid) |
| } |
| } |
| } |
| |
| func TestLookupGroupIdServiceAccount(t *testing.T) { |
| t.Parallel() |
| for _, tt := range serviceAccounts { |
| u, err := LookupGroupId(tt.sid) |
| if err != nil { |
| t.Errorf("LookupGroupId(%q): %v", tt.sid, err) |
| continue |
| } |
| if u.Gid != tt.sid { |
| t.Errorf("unexpected gid for %q; got %q, want %q", u.Name, u.Gid, tt.sid) |
| } |
| } |
| } |