1 // Copyright 2012 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 "errors"
9 "fmt"
10 "internal/syscall/windows"
11 "internal/syscall/windows/registry"
12 "runtime"
13 "syscall"
14 "unsafe"
15 )
16 17 func isDomainJoined() (bool, error) {
18 var domain *uint16
19 var status uint32
20 err := syscall.NetGetJoinInformation(nil, &domain, &status)
21 if err != nil {
22 return false, err
23 }
24 syscall.NetApiBufferFree((*byte)(unsafe.Pointer(domain)))
25 return status == syscall.NetSetupDomainName, nil
26 }
27 28 func lookupFullNameDomain(domainAndUser string) (string, error) {
29 return syscall.TranslateAccountName(domainAndUser,
30 syscall.NameSamCompatible, syscall.NameDisplay, 50)
31 }
32 33 func lookupFullNameServer(servername, username string) (string, error) {
34 s, e := syscall.UTF16PtrFromString(servername)
35 if e != nil {
36 return "", e
37 }
38 u, e := syscall.UTF16PtrFromString(username)
39 if e != nil {
40 return "", e
41 }
42 var p *byte
43 e = syscall.NetUserGetInfo(s, u, 10, &p)
44 if e != nil {
45 return "", e
46 }
47 defer syscall.NetApiBufferFree(p)
48 i := (*syscall.UserInfo10)(unsafe.Pointer(p))
49 return windows.UTF16PtrToString(i.FullName), nil
50 }
51 52 func lookupFullName(domain, username, domainAndUser string) (string, error) {
53 joined, err := isDomainJoined()
54 if err == nil && joined {
55 name, err := lookupFullNameDomain(domainAndUser)
56 if err == nil {
57 return name, nil
58 }
59 }
60 name, err := lookupFullNameServer(domain, username)
61 if err == nil {
62 return name, nil
63 }
64 // domain worked neither as a domain nor as a server
65 // could be domain server unavailable
66 // pretend username is fullname
67 return username, nil
68 }
69 70 // getProfilesDirectory retrieves the path to the root directory
71 // where user profiles are stored.
72 func getProfilesDirectory() (string, error) {
73 n := uint32(100)
74 for {
75 b := make([]uint16, n)
76 e := windows.GetProfilesDirectory(&b[0], &n)
77 if e == nil {
78 return syscall.UTF16ToString(b), nil
79 }
80 if e != syscall.ERROR_INSUFFICIENT_BUFFER {
81 return "", e
82 }
83 if n <= uint32(len(b)) {
84 return "", e
85 }
86 }
87 }
88 89 func isServiceAccount(sid *syscall.SID) bool {
90 if !windows.IsValidSid(sid) {
91 // We don't accept SIDs from the public API, so this should never happen.
92 // Better be on the safe side and validate anyway.
93 return false
94 }
95 // The following RIDs are considered service user accounts as per
96 // https://learn.microsoft.com/en-us/windows/win32/secauthz/well-known-sids and
97 // https://learn.microsoft.com/en-us/windows/win32/services/service-user-accounts:
98 // - "S-1-5-18": LocalSystem
99 // - "S-1-5-19": LocalService
100 // - "S-1-5-20": NetworkService
101 if windows.GetSidSubAuthorityCount(sid) != windows.SID_REVISION ||
102 windows.GetSidIdentifierAuthority(sid) != windows.SECURITY_NT_AUTHORITY {
103 return false
104 }
105 switch windows.GetSidSubAuthority(sid, 0) {
106 case windows.SECURITY_LOCAL_SYSTEM_RID,
107 windows.SECURITY_LOCAL_SERVICE_RID,
108 windows.SECURITY_NETWORK_SERVICE_RID:
109 return true
110 }
111 return false
112 }
113 114 func isValidUserAccountType(sid *syscall.SID, sidType uint32) bool {
115 switch sidType {
116 case syscall.SidTypeUser:
117 return true
118 case syscall.SidTypeWellKnownGroup:
119 return isServiceAccount(sid)
120 }
121 return false
122 }
123 124 func isValidGroupAccountType(sidType uint32) bool {
125 switch sidType {
126 case syscall.SidTypeGroup:
127 return true
128 case syscall.SidTypeWellKnownGroup:
129 // Some well-known groups are also considered service accounts,
130 // so isValidUserAccountType would return true for them.
131 // We have historically allowed them in LookupGroup and LookupGroupId,
132 // so don't treat them as invalid here.
133 return true
134 case syscall.SidTypeAlias:
135 // https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-samr/7b2aeb27-92fc-41f6-8437-deb65d950921#gt_0387e636-5654-4910-9519-1f8326cf5ec0
136 // SidTypeAlias should also be treated as a group type next to SidTypeGroup
137 // and SidTypeWellKnownGroup:
138 // "alias object -> resource group: A group object..."
139 //
140 // Tests show that "Administrators" can be considered of type SidTypeAlias.
141 return true
142 }
143 return false
144 }
145 146 // lookupUsernameAndDomain obtains the username and domain for usid.
147 func lookupUsernameAndDomain(usid *syscall.SID) (username, domain string, sidType uint32, e error) {
148 username, domain, sidType, e = usid.LookupAccount("")
149 if e != nil {
150 return "", "", 0, e
151 }
152 if !isValidUserAccountType(usid, sidType) {
153 return "", "", 0, fmt.Errorf("user: should be user account type, not %d", sidType)
154 }
155 return username, domain, sidType, nil
156 }
157 158 // findHomeDirInRegistry finds the user home path based on the uid.
159 func findHomeDirInRegistry(uid string) (dir string, e error) {
160 k, e := registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList\`+uid, registry.QUERY_VALUE)
161 if e != nil {
162 return "", e
163 }
164 defer k.Close()
165 dir, _, e = k.GetStringValue("ProfileImagePath")
166 if e != nil {
167 return "", e
168 }
169 return dir, nil
170 }
171 172 // lookupGroupName accepts the name of a group and retrieves the group SID.
173 func lookupGroupName(groupname string) (string, error) {
174 sid, _, t, e := syscall.LookupSID("", groupname)
175 if e != nil {
176 return "", e
177 }
178 if !isValidGroupAccountType(t) {
179 return "", fmt.Errorf("lookupGroupName: should be group account type, not %d", t)
180 }
181 return sid.String()
182 }
183 184 // listGroupsForUsernameAndDomain accepts username and domain and retrieves
185 // a SID list of the local groups where this user is a member.
186 func listGroupsForUsernameAndDomain(username, domain string) ([]string, error) {
187 // Check if both the domain name and user should be used.
188 var query string
189 joined, err := isDomainJoined()
190 if err == nil && joined && len(domain) != 0 {
191 query = domain + `\` + username
192 } else {
193 query = username
194 }
195 q, err := syscall.UTF16PtrFromString(query)
196 if err != nil {
197 return nil, err
198 }
199 var p0 *byte
200 var entriesRead, totalEntries uint32
201 // https://learn.microsoft.com/en-us/windows/win32/api/lmaccess/nf-lmaccess-netusergetlocalgroups
202 // NetUserGetLocalGroups() would return a list of LocalGroupUserInfo0
203 // elements which hold the names of local groups where the user participates.
204 // The list does not follow any sorting order.
205 err = windows.NetUserGetLocalGroups(nil, q, 0, windows.LG_INCLUDE_INDIRECT, &p0, windows.MAX_PREFERRED_LENGTH, &entriesRead, &totalEntries)
206 if err != nil {
207 return nil, err
208 }
209 defer syscall.NetApiBufferFree(p0)
210 if entriesRead == 0 {
211 return nil, nil
212 }
213 entries := (*[1024]windows.LocalGroupUserInfo0)(unsafe.Pointer(p0))[:entriesRead:entriesRead]
214 var sids []string
215 for _, entry := range entries {
216 if entry.Name == nil {
217 continue
218 }
219 sid, err := lookupGroupName(windows.UTF16PtrToString(entry.Name))
220 if err != nil {
221 return nil, err
222 }
223 sids = append(sids, sid)
224 }
225 return sids, nil
226 }
227 228 func newUser(uid, gid, dir, username, domain string) (*User, error) {
229 domainAndUser := domain + `\` + username
230 name, e := lookupFullName(domain, username, domainAndUser)
231 if e != nil {
232 return nil, e
233 }
234 u := &User{
235 Uid: uid,
236 Gid: gid,
237 Username: domainAndUser,
238 Name: name,
239 HomeDir: dir,
240 }
241 return u, nil
242 }
243 244 var (
245 // unused variables (in this implementation)
246 // modified during test to exercise code paths in the cgo implementation.
247 userBuffer = 0
248 groupBuffer = 0
249 )
250 251 func current() (*User, error) {
252 // Use runAsProcessOwner to ensure that we can access the process token
253 // when calling syscall.OpenCurrentProcessToken if the current thread
254 // is impersonating a different user. See https://go.dev/issue/68647.
255 var usr *User
256 err := runAsProcessOwner(func() error {
257 t, e := syscall.OpenCurrentProcessToken()
258 if e != nil {
259 return e
260 }
261 defer t.Close()
262 u, e := t.GetTokenUser()
263 if e != nil {
264 return e
265 }
266 pg, e := t.GetTokenPrimaryGroup()
267 if e != nil {
268 return e
269 }
270 uid, e := u.User.Sid.String()
271 if e != nil {
272 return e
273 }
274 gid, e := pg.PrimaryGroup.String()
275 if e != nil {
276 return e
277 }
278 dir, e := t.GetUserProfileDirectory()
279 if e != nil {
280 return e
281 }
282 username, e := windows.GetUserName(syscall.NameSamCompatible)
283 if e != nil {
284 return e
285 }
286 displayName, e := windows.GetUserName(syscall.NameDisplay)
287 if e != nil {
288 // Historically, the username is used as fallback
289 // when the display name can't be retrieved.
290 displayName = username
291 }
292 usr = &User{
293 Uid: uid,
294 Gid: gid,
295 Username: username,
296 Name: displayName,
297 HomeDir: dir,
298 }
299 return nil
300 })
301 return usr, err
302 }
303 304 // runAsProcessOwner runs f in the context of the current process owner,
305 // that is, removing any impersonation that may be in effect before calling f,
306 // and restoring the impersonation afterwards.
307 func runAsProcessOwner(f func() error) error {
308 var impersonationRollbackErr error
309 runtime.LockOSThread()
310 defer func() {
311 // If impersonation failed, the thread is running with the wrong token,
312 // so it's better to terminate it.
313 // This is achieved by not calling runtime.UnlockOSThread.
314 if impersonationRollbackErr != nil {
315 println("os/user: failed to revert to previous token:", impersonationRollbackErr.Error())
316 runtime.Goexit()
317 } else {
318 runtime.UnlockOSThread()
319 }
320 }()
321 prevToken, isProcessToken, err := getCurrentToken()
322 if err != nil {
323 return fmt.Errorf("os/user: failed to get current token: %w", err)
324 }
325 defer prevToken.Close()
326 if !isProcessToken {
327 if err = windows.RevertToSelf(); err != nil {
328 return fmt.Errorf("os/user: failed to revert to self: %w", err)
329 }
330 defer func() {
331 impersonationRollbackErr = windows.ImpersonateLoggedOnUser(prevToken)
332 }()
333 }
334 return f()
335 }
336 337 // getCurrentToken returns the current thread token, or
338 // the process token if the thread doesn't have a token.
339 func getCurrentToken() (t syscall.Token, isProcessToken bool, err error) {
340 thread, _ := windows.GetCurrentThread()
341 // Need TOKEN_DUPLICATE and TOKEN_IMPERSONATE to use the token in ImpersonateLoggedOnUser.
342 err = windows.OpenThreadToken(thread, syscall.TOKEN_QUERY|syscall.TOKEN_DUPLICATE|syscall.TOKEN_IMPERSONATE, true, &t)
343 if errors.Is(err, windows.ERROR_NO_TOKEN) {
344 // Not impersonating, use the process token.
345 isProcessToken = true
346 t, err = syscall.OpenCurrentProcessToken()
347 }
348 return t, isProcessToken, err
349 }
350 351 // lookupUserPrimaryGroup obtains the primary group SID for a user using this method:
352 // https://support.microsoft.com/en-us/help/297951/how-to-use-the-primarygroupid-attribute-to-find-the-primary-group-for
353 // The method follows this formula: domainRID + "-" + primaryGroupRID
354 func lookupUserPrimaryGroup(username, domain string) (string, error) {
355 // get the domain RID
356 sid, _, t, e := syscall.LookupSID("", domain)
357 if e != nil {
358 return "", e
359 }
360 if t != syscall.SidTypeDomain {
361 return "", fmt.Errorf("lookupUserPrimaryGroup: should be domain account type, not %d", t)
362 }
363 domainRID, e := sid.String()
364 if e != nil {
365 return "", e
366 }
367 // If the user has joined a domain use the RID of the default primary group
368 // called "Domain Users":
369 // https://support.microsoft.com/en-us/help/243330/well-known-security-identifiers-in-windows-operating-systems
370 // SID: S-1-5-21domain-513
371 //
372 // The correct way to obtain the primary group of a domain user is
373 // probing the user primaryGroupID attribute in the server Active Directory:
374 // https://learn.microsoft.com/en-us/windows/win32/adschema/a-primarygroupid
375 //
376 // Note that the primary group of domain users should not be modified
377 // on Windows for performance reasons, even if it's possible to do that.
378 // The .NET Developer's Guide to Directory Services Programming - Page 409
379 // 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
380 joined, err := isDomainJoined()
381 if err == nil && joined {
382 return domainRID + "-513", nil
383 }
384 // For non-domain users call NetUserGetInfo() with level 4, which
385 // in this case would not have any network overhead.
386 // The primary group should not change from RID 513 here either
387 // but the group will be called "None" instead:
388 // https://www.adampalmer.me/iodigitalsec/2013/08/10/windows-null-session-enumeration/
389 // "Group 'None' (RID: 513)"
390 u, e := syscall.UTF16PtrFromString(username)
391 if e != nil {
392 return "", e
393 }
394 d, e := syscall.UTF16PtrFromString(domain)
395 if e != nil {
396 return "", e
397 }
398 var p *byte
399 e = syscall.NetUserGetInfo(d, u, 4, &p)
400 if e != nil {
401 return "", e
402 }
403 defer syscall.NetApiBufferFree(p)
404 i := (*windows.UserInfo4)(unsafe.Pointer(p))
405 return fmt.Sprintf("%s-%d", domainRID, i.PrimaryGroupID), nil
406 }
407 408 func newUserFromSid(usid *syscall.SID) (*User, error) {
409 username, domain, sidType, e := lookupUsernameAndDomain(usid)
410 if e != nil {
411 return nil, e
412 }
413 uid, e := usid.String()
414 if e != nil {
415 return nil, e
416 }
417 var gid string
418 if sidType == syscall.SidTypeWellKnownGroup {
419 // The SID does not contain a domain; this function's domain variable has
420 // been populated with the SID's identifier authority. This happens with
421 // special service user accounts such as "NT AUTHORITY\LocalSystem".
422 // In this case, gid is the same as the user SID.
423 gid = uid
424 } else {
425 gid, e = lookupUserPrimaryGroup(username, domain)
426 if e != nil {
427 return nil, e
428 }
429 }
430 // If this user has logged in at least once their home path should be stored
431 // in the registry under the specified SID. References:
432 // https://social.technet.microsoft.com/wiki/contents/articles/13895.how-to-remove-a-corrupted-user-profile-from-the-registry.aspx
433 // https://support.asperasoft.com/hc/en-us/articles/216127438-How-to-delete-Windows-user-profiles
434 //
435 // The registry is the most reliable way to find the home path as the user
436 // might have decided to move it outside of the default location,
437 // (e.g. C:\users). Reference:
438 // https://answers.microsoft.com/en-us/windows/forum/windows_7-security/how-do-i-set-a-home-directory-outside-cusers-for-a/aed68262-1bf4-4a4d-93dc-7495193a440f
439 dir, e := findHomeDirInRegistry(uid)
440 if e != nil {
441 // If the home path does not exist in the registry, the user might
442 // have not logged in yet; fall back to using getProfilesDirectory().
443 // Find the username based on a SID and append that to the result of
444 // getProfilesDirectory(). The domain is not relevant here.
445 dir, e = getProfilesDirectory()
446 if e != nil {
447 return nil, e
448 }
449 dir += `\` + username
450 }
451 return newUser(uid, gid, dir, username, domain)
452 }
453 454 func lookupUser(username string) (*User, error) {
455 sid, _, t, e := syscall.LookupSID("", username)
456 if e != nil {
457 return nil, e
458 }
459 if !isValidUserAccountType(sid, t) {
460 return nil, fmt.Errorf("user: should be user account type, not %d", t)
461 }
462 return newUserFromSid(sid)
463 }
464 465 func lookupUserId(uid string) (*User, error) {
466 sid, e := syscall.StringToSid(uid)
467 if e != nil {
468 return nil, e
469 }
470 return newUserFromSid(sid)
471 }
472 473 func lookupGroup(groupname string) (*Group, error) {
474 sid, err := lookupGroupName(groupname)
475 if err != nil {
476 return nil, err
477 }
478 return &Group{Name: groupname, Gid: sid}, nil
479 }
480 481 func lookupGroupId(gid string) (*Group, error) {
482 sid, err := syscall.StringToSid(gid)
483 if err != nil {
484 return nil, err
485 }
486 groupname, _, t, err := sid.LookupAccount("")
487 if err != nil {
488 return nil, err
489 }
490 if !isValidGroupAccountType(t) {
491 return nil, fmt.Errorf("lookupGroupId: should be group account type, not %d", t)
492 }
493 return &Group{Name: groupname, Gid: gid}, nil
494 }
495 496 func listGroups(user *User) ([]string, error) {
497 var sids []string
498 if u, err := Current(); err == nil && u.Uid == user.Uid {
499 // It is faster and more reliable to get the groups
500 // of the current user from the current process token.
501 err := runAsProcessOwner(func() error {
502 t, err := syscall.OpenCurrentProcessToken()
503 if err != nil {
504 return err
505 }
506 defer t.Close()
507 groups, err := windows.GetTokenGroups(t)
508 if err != nil {
509 return err
510 }
511 for _, g := range groups.AllGroups() {
512 sid, err := g.Sid.String()
513 if err != nil {
514 return err
515 }
516 sids = append(sids, sid)
517 }
518 return nil
519 })
520 if err != nil {
521 return nil, err
522 }
523 } else {
524 sid, err := syscall.StringToSid(user.Uid)
525 if err != nil {
526 return nil, err
527 }
528 username, domain, _, err := lookupUsernameAndDomain(sid)
529 if err != nil {
530 return nil, err
531 }
532 sids, err = listGroupsForUsernameAndDomain(username, domain)
533 if err != nil {
534 return nil, err
535 }
536 }
537 // Add the primary group of the user to the list if it is not already there.
538 // This is done only to comply with the POSIX concept of a primary group.
539 for _, sid := range sids {
540 if sid == user.Gid {
541 return sids, nil
542 }
543 }
544 return append(sids, user.Gid), nil
545 }
546