Source file src/os/user/user_windows_test.go

     1  // Copyright 2024 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package user
     6  
     7  import (
     8  	"crypto/rand"
     9  	"encoding/base64"
    10  	"encoding/binary"
    11  	"errors"
    12  	"fmt"
    13  	"internal/syscall/windows"
    14  	"internal/testenv"
    15  	"os"
    16  	"os/exec"
    17  	"runtime"
    18  	"strconv"
    19  	"strings"
    20  	"syscall"
    21  	"testing"
    22  	"unicode"
    23  	"unicode/utf8"
    24  	"unsafe"
    25  )
    26  
    27  // addUserAccount creates a local user account.
    28  // It returns the name and password of the new account.
    29  // Multiple programs or goroutines calling addUserAccount simultaneously will not choose the same directory.
    30  func addUserAccount(t *testing.T) (name, password string) {
    31  	t.TempDir()
    32  	pattern := t.Name()
    33  	// Windows limits the user name to 20 characters,
    34  	// leave space for a 4 digits random suffix.
    35  	const maxNameLen, suffixLen = 20, 4
    36  	pattern = pattern[:min(len(pattern), maxNameLen-suffixLen)]
    37  	// Drop unusual characters from the account name.
    38  	mapper := func(r rune) rune {
    39  		if r < utf8.RuneSelf {
    40  			if '0' <= r && r <= '9' ||
    41  				'a' <= r && r <= 'z' ||
    42  				'A' <= r && r <= 'Z' {
    43  				return r
    44  			}
    45  		} else if unicode.IsLetter(r) || unicode.IsNumber(r) {
    46  			return r
    47  		}
    48  		return -1
    49  	}
    50  	pattern = strings.Map(mapper, pattern)
    51  
    52  	// Generate a long random password.
    53  	var pwd [33]byte
    54  	rand.Read(pwd[:])
    55  	// Add special chars to ensure it satisfies password requirements.
    56  	password = base64.StdEncoding.EncodeToString(pwd[:]) + "_-As@!%*(1)4#2"
    57  	password16, err := syscall.UTF16PtrFromString(password)
    58  	if err != nil {
    59  		t.Fatal(err)
    60  	}
    61  
    62  	try := 0
    63  	for {
    64  		// Calculate a random suffix to append to the user name.
    65  		var suffix [2]byte
    66  		rand.Read(suffix[:])
    67  		suffixStr := strconv.FormatUint(uint64(binary.LittleEndian.Uint16(suffix[:])), 10)
    68  		name := pattern + suffixStr[:min(len(suffixStr), suffixLen)]
    69  		name16, err := syscall.UTF16PtrFromString(name)
    70  		if err != nil {
    71  			t.Fatal(err)
    72  		}
    73  		// Create user.
    74  		userInfo := windows.UserInfo1{
    75  			Name:     name16,
    76  			Password: password16,
    77  			Priv:     windows.USER_PRIV_USER,
    78  		}
    79  		err = windows.NetUserAdd(nil, 1, (*byte)(unsafe.Pointer(&userInfo)), nil)
    80  		if errors.Is(err, syscall.ERROR_ACCESS_DENIED) {
    81  			t.Skip("skipping test; don't have permission to create user")
    82  		}
    83  		// If the user already exists, try again with a different name.
    84  		if errors.Is(err, windows.NERR_UserExists) {
    85  			if try++; try < 1000 {
    86  				t.Log("user already exists, trying again with a different name")
    87  				continue
    88  			}
    89  		}
    90  		if err != nil {
    91  			t.Fatalf("NetUserAdd failed: %v", err)
    92  		}
    93  		// Delete the user when the test is done.
    94  		t.Cleanup(func() {
    95  			if err := windows.NetUserDel(nil, name16); err != nil {
    96  				if !errors.Is(err, windows.NERR_UserNotFound) {
    97  					t.Fatal(err)
    98  				}
    99  			}
   100  		})
   101  		return name, password
   102  	}
   103  }
   104  
   105  // windowsTestAccount creates a test user and returns a token for that user.
   106  // If the user already exists, it will be deleted and recreated.
   107  // The caller is responsible for closing the token.
   108  func windowsTestAccount(t *testing.T) (syscall.Token, *User) {
   109  	if testenv.Builder() == "" {
   110  		// Adding and deleting users requires special permissions.
   111  		// Even if we have them, we don't want to create users on
   112  		// on dev machines, as they may not be cleaned up.
   113  		// See https://dev.go/issue/70396.
   114  		t.Skip("skipping non-hermetic test outside of Go builders")
   115  	}
   116  	name, password := addUserAccount(t)
   117  	name16, err := syscall.UTF16PtrFromString(name)
   118  	if err != nil {
   119  		t.Fatal(err)
   120  	}
   121  	pwd16, err := syscall.UTF16PtrFromString(password)
   122  	if err != nil {
   123  		t.Fatal(err)
   124  	}
   125  	domain, err := syscall.UTF16PtrFromString(".")
   126  	if err != nil {
   127  		t.Fatal(err)
   128  	}
   129  	const LOGON32_PROVIDER_DEFAULT = 0
   130  	const LOGON32_LOGON_INTERACTIVE = 2
   131  	var token syscall.Token
   132  	if err = windows.LogonUser(name16, domain, pwd16, LOGON32_LOGON_INTERACTIVE, LOGON32_PROVIDER_DEFAULT, &token); err != nil {
   133  		t.Fatal(err)
   134  	}
   135  	t.Cleanup(func() {
   136  		token.Close()
   137  	})
   138  	usr, err := Lookup(name)
   139  	if err != nil {
   140  		t.Fatal(err)
   141  	}
   142  	return token, usr
   143  }
   144  
   145  func TestImpersonatedSelf(t *testing.T) {
   146  	runtime.LockOSThread()
   147  	defer runtime.UnlockOSThread()
   148  
   149  	want, err := current()
   150  	if err != nil {
   151  		t.Fatal(err)
   152  	}
   153  
   154  	levels := []uint32{
   155  		windows.SecurityAnonymous,
   156  		windows.SecurityIdentification,
   157  		windows.SecurityImpersonation,
   158  		windows.SecurityDelegation,
   159  	}
   160  	for _, level := range levels {
   161  		t.Run(strconv.Itoa(int(level)), func(t *testing.T) {
   162  			if err = windows.ImpersonateSelf(level); err != nil {
   163  				t.Fatal(err)
   164  			}
   165  			defer windows.RevertToSelf()
   166  
   167  			got, err := current()
   168  			if level == windows.SecurityAnonymous {
   169  				// We can't get the process token when using an anonymous token,
   170  				// so we expect an error here.
   171  				if err == nil {
   172  					t.Fatal("expected error")
   173  				}
   174  				return
   175  			}
   176  			if err != nil {
   177  				t.Fatal(err)
   178  			}
   179  			compare(t, want, got)
   180  		})
   181  	}
   182  }
   183  
   184  func TestImpersonated(t *testing.T) {
   185  	runtime.LockOSThread()
   186  	defer runtime.UnlockOSThread()
   187  
   188  	want, err := current()
   189  	if err != nil {
   190  		t.Fatal(err)
   191  	}
   192  
   193  	// Create a test user and log in as that user.
   194  	token, _ := windowsTestAccount(t)
   195  
   196  	// Impersonate the test user.
   197  	if err = windows.ImpersonateLoggedOnUser(token); err != nil {
   198  		t.Fatal(err)
   199  	}
   200  	defer func() {
   201  		err = windows.RevertToSelf()
   202  		if err != nil {
   203  			// If we can't revert to self, we can't continue testing.
   204  			panic(err)
   205  		}
   206  	}()
   207  
   208  	got, err := current()
   209  	if err != nil {
   210  		t.Fatal(err)
   211  	}
   212  	compare(t, want, got)
   213  }
   214  
   215  func TestCurrentNetapi32(t *testing.T) {
   216  	if os.Getenv("GO_WANT_HELPER_PROCESS") == "1" {
   217  		// Test that Current does not load netapi32.dll.
   218  		// First call Current.
   219  		Current()
   220  
   221  		// Then check if netapi32.dll is loaded.
   222  		netapi32, err := syscall.UTF16PtrFromString("netapi32.dll")
   223  		if err != nil {
   224  			fmt.Fprintf(os.Stderr, "error: %s\n", err.Error())
   225  			os.Exit(9)
   226  			return
   227  		}
   228  		mod, _ := windows.GetModuleHandle(netapi32)
   229  		if mod != 0 {
   230  			fmt.Fprintf(os.Stderr, "netapi32.dll is loaded\n")
   231  			os.Exit(9)
   232  			return
   233  		}
   234  		os.Exit(0)
   235  		return
   236  	}
   237  	exe := testenv.Executable(t)
   238  	cmd := testenv.CleanCmdEnv(exec.Command(exe, "-test.run=^TestCurrentNetapi32$"))
   239  	cmd.Env = append(cmd.Env, "GO_WANT_HELPER_PROCESS=1")
   240  	out, err := cmd.CombinedOutput()
   241  	if err != nil {
   242  		t.Fatalf("%v\n%s", err, out)
   243  	}
   244  }
   245  
   246  func TestGroupIdsTestUser(t *testing.T) {
   247  	// Create a test user and log in as that user.
   248  	_, user := windowsTestAccount(t)
   249  
   250  	gids, err := user.GroupIds()
   251  	if err != nil {
   252  		t.Fatal(err)
   253  	}
   254  
   255  	if err != nil {
   256  		t.Fatalf("%+v.GroupIds(): %v", user, err)
   257  	}
   258  	if !containsID(gids, user.Gid) {
   259  		t.Errorf("%+v.GroupIds() = %v; does not contain user GID %s", user, gids, user.Gid)
   260  	}
   261  }
   262  
   263  var serviceAccounts = []struct {
   264  	sid  string
   265  	name string
   266  }{
   267  	{"S-1-5-18", "NT AUTHORITY\\SYSTEM"},
   268  	{"S-1-5-19", "NT AUTHORITY\\LOCAL SERVICE"},
   269  	{"S-1-5-20", "NT AUTHORITY\\NETWORK SERVICE"},
   270  }
   271  
   272  func TestLookupServiceAccount(t *testing.T) {
   273  	t.Parallel()
   274  	for _, tt := range serviceAccounts {
   275  		u, err := Lookup(tt.name)
   276  		if err != nil {
   277  			t.Errorf("Lookup(%q): %v", tt.name, err)
   278  			continue
   279  		}
   280  		if u.Uid != tt.sid {
   281  			t.Errorf("unexpected uid for %q; got %q, want %q", u.Name, u.Uid, tt.sid)
   282  		}
   283  	}
   284  }
   285  
   286  func TestLookupIdServiceAccount(t *testing.T) {
   287  	t.Parallel()
   288  	for _, tt := range serviceAccounts {
   289  		u, err := LookupId(tt.sid)
   290  		if err != nil {
   291  			t.Errorf("LookupId(%q): %v", tt.sid, err)
   292  			continue
   293  		}
   294  		if u.Gid != tt.sid {
   295  			t.Errorf("unexpected gid for %q; got %q, want %q", u.Name, u.Gid, tt.sid)
   296  		}
   297  		if u.Username != tt.name {
   298  			t.Errorf("unexpected user name for %q; got %q, want %q", u.Gid, u.Username, tt.name)
   299  		}
   300  	}
   301  }
   302  
   303  func TestLookupGroupServiceAccount(t *testing.T) {
   304  	t.Parallel()
   305  	for _, tt := range serviceAccounts {
   306  		u, err := LookupGroup(tt.name)
   307  		if err != nil {
   308  			t.Errorf("LookupGroup(%q): %v", tt.name, err)
   309  			continue
   310  		}
   311  		if u.Gid != tt.sid {
   312  			t.Errorf("unexpected gid for %q; got %q, want %q", u.Name, u.Gid, tt.sid)
   313  		}
   314  	}
   315  }
   316  
   317  func TestLookupGroupIdServiceAccount(t *testing.T) {
   318  	t.Parallel()
   319  	for _, tt := range serviceAccounts {
   320  		u, err := LookupGroupId(tt.sid)
   321  		if err != nil {
   322  			t.Errorf("LookupGroupId(%q): %v", tt.sid, err)
   323  			continue
   324  		}
   325  		if u.Gid != tt.sid {
   326  			t.Errorf("unexpected gid for %q; got %q, want %q", u.Name, u.Gid, tt.sid)
   327  		}
   328  	}
   329  }
   330  

View as plain text