| // 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" |
| "errors" |
| "fmt" |
| "internal/syscall/windows" |
| "internal/testenv" |
| "os" |
| "os/exec" |
| "runtime" |
| "slices" |
| "strconv" |
| "syscall" |
| "testing" |
| "unsafe" |
| ) |
| |
| // 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") |
| } |
| const testUserName = "GoStdTestUser01" |
| var password [33]byte |
| rand.Read(password[:]) |
| // Add special chars to ensure it satisfies password requirements. |
| pwd := base64.StdEncoding.EncodeToString(password[:]) + "_-As@!%*(1)4#2" |
| name, err := syscall.UTF16PtrFromString(testUserName) |
| if err != nil { |
| t.Fatal(err) |
| } |
| pwd16, err := syscall.UTF16PtrFromString(pwd) |
| if err != nil { |
| t.Fatal(err) |
| } |
| userInfo := windows.UserInfo1{ |
| Name: name, |
| Password: pwd16, |
| Priv: windows.USER_PRIV_USER, |
| } |
| // Create 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 errors.Is(err, windows.NERR_UserExists) { |
| // User already exists, delete and recreate. |
| if err = windows.NetUserDel(nil, name); err != nil { |
| t.Fatal(err) |
| } |
| if err = windows.NetUserAdd(nil, 1, (*byte)(unsafe.Pointer(&userInfo)), nil); err != nil { |
| t.Fatal(err) |
| } |
| } else if err != nil { |
| t.Fatal(err) |
| } |
| t.Cleanup(func() { |
| if err = windows.NetUserDel(nil, name); err != nil { |
| if !errors.Is(err, windows.NERR_UserNotFound) { |
| 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(name, domain, pwd16, LOGON32_LOGON_INTERACTIVE, LOGON32_PROVIDER_DEFAULT, &token); err != nil { |
| t.Fatal(err) |
| } |
| t.Cleanup(func() { |
| token.Close() |
| }) |
| usr, err := Lookup(testUserName) |
| 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) |
| } |
| } |
| } |