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
5package user
6
7import (
8	"fmt"
9	"internal/syscall/windows"
10	"internal/syscall/windows/registry"
11	"syscall"
12	"unsafe"
13)
14
15func isDomainJoined() (bool, error) {
16	var domain *uint16
17	var status uint32
18	err := syscall.NetGetJoinInformation(nil, &domain, &status)
19	if err != nil {
20		return false, err
21	}
22	syscall.NetApiBufferFree((*byte)(unsafe.Pointer(domain)))
23	return status == syscall.NetSetupDomainName, nil
24}
25
26func lookupFullNameDomain(domainAndUser string) (string, error) {
27	return syscall.TranslateAccountName(domainAndUser,
28		syscall.NameSamCompatible, syscall.NameDisplay, 50)
29}
30
31func lookupFullNameServer(servername, username string) (string, error) {
32	s, e := syscall.UTF16PtrFromString(servername)
33	if e != nil {
34		return "", e
35	}
36	u, e := syscall.UTF16PtrFromString(username)
37	if e != nil {
38		return "", e
39	}
40	var p *byte
41	e = syscall.NetUserGetInfo(s, u, 10, &p)
42	if e != nil {
43		return "", e
44	}
45	defer syscall.NetApiBufferFree(p)
46	i := (*syscall.UserInfo10)(unsafe.Pointer(p))
47	return windows.UTF16PtrToString(i.FullName), nil
48}
49
50func lookupFullName(domain, username, domainAndUser string) (string, error) {
51	joined, err := isDomainJoined()
52	if err == nil && joined {
53		name, err := lookupFullNameDomain(domainAndUser)
54		if err == nil {
55			return name, nil
56		}
57	}
58	name, err := lookupFullNameServer(domain, username)
59	if err == nil {
60		return name, nil
61	}
62	// domain worked neither as a domain nor as a server
63	// could be domain server unavailable
64	// pretend username is fullname
65	return username, nil
66}
67
68// getProfilesDirectory retrieves the path to the root directory
69// where user profiles are stored.
70func getProfilesDirectory() (string, error) {
71	n := uint32(100)
72	for {
73		b := make([]uint16, n)
74		e := windows.GetProfilesDirectory(&b[0], &n)
75		if e == nil {
76			return syscall.UTF16ToString(b), nil
77		}
78		if e != syscall.ERROR_INSUFFICIENT_BUFFER {
79			return "", e
80		}
81		if n <= uint32(len(b)) {
82			return "", e
83		}
84	}
85}
86
87// lookupUsernameAndDomain obtains the username and domain for usid.
88func lookupUsernameAndDomain(usid *syscall.SID) (username, domain string, e error) {
89	username, domain, t, e := usid.LookupAccount("")
90	if e != nil {
91		return "", "", e
92	}
93	if t != syscall.SidTypeUser {
94		return "", "", fmt.Errorf("user: should be user account type, not %d", t)
95	}
96	return username, domain, nil
97}
98
99// findHomeDirInRegistry finds the user home path based on the uid.
100func findHomeDirInRegistry(uid string) (dir string, e error) {
101	k, e := registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList\`+uid, registry.QUERY_VALUE)
102	if e != nil {
103		return "", e
104	}
105	defer k.Close()
106	dir, _, e = k.GetStringValue("ProfileImagePath")
107	if e != nil {
108		return "", e
109	}
110	return dir, nil
111}
112
113// lookupGroupName accepts the name of a group and retrieves the group SID.
114func lookupGroupName(groupname string) (string, error) {
115	sid, _, t, e := syscall.LookupSID("", groupname)
116	if e != nil {
117		return "", e
118	}
119	// https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-samr/7b2aeb27-92fc-41f6-8437-deb65d950921#gt_0387e636-5654-4910-9519-1f8326cf5ec0
120	// SidTypeAlias should also be treated as a group type next to SidTypeGroup
121	// and SidTypeWellKnownGroup:
122	// "alias object -> resource group: A group object..."
123	//
124	// Tests show that "Administrators" can be considered of type SidTypeAlias.
125	if t != syscall.SidTypeGroup && t != syscall.SidTypeWellKnownGroup && t != syscall.SidTypeAlias {
126		return "", fmt.Errorf("lookupGroupName: should be group account type, not %d", t)
127	}
128	return sid.String()
129}
130
131// listGroupsForUsernameAndDomain accepts username and domain and retrieves
132// a SID list of the local groups where this user is a member.
133func listGroupsForUsernameAndDomain(username, domain string) ([]string, error) {
134	// Check if both the domain name and user should be used.
135	var query string
136	joined, err := isDomainJoined()
137	if err == nil && joined && len(domain) != 0 {
138		query = domain + `\` + username
139	} else {
140		query = username
141	}
142	q, err := syscall.UTF16PtrFromString(query)
143	if err != nil {
144		return nil, err
145	}
146	var p0 *byte
147	var entriesRead, totalEntries uint32
148	// https://learn.microsoft.com/en-us/windows/win32/api/lmaccess/nf-lmaccess-netusergetlocalgroups
149	// NetUserGetLocalGroups() would return a list of LocalGroupUserInfo0
150	// elements which hold the names of local groups where the user participates.
151	// The list does not follow any sorting order.
152	//
153	// If no groups can be found for this user, NetUserGetLocalGroups() should
154	// always return the SID of a single group called "None", which
155	// also happens to be the primary group for the local user.
156	err = windows.NetUserGetLocalGroups(nil, q, 0, windows.LG_INCLUDE_INDIRECT, &p0, windows.MAX_PREFERRED_LENGTH, &entriesRead, &totalEntries)
157	if err != nil {
158		return nil, err
159	}
160	defer syscall.NetApiBufferFree(p0)
161	if entriesRead == 0 {
162		return nil, fmt.Errorf("listGroupsForUsernameAndDomain: NetUserGetLocalGroups() returned an empty list for domain: %s, username: %s", domain, username)
163	}
164	entries := (*[1024]windows.LocalGroupUserInfo0)(unsafe.Pointer(p0))[:entriesRead:entriesRead]
165	var sids []string
166	for _, entry := range entries {
167		if entry.Name == nil {
168			continue
169		}
170		sid, err := lookupGroupName(windows.UTF16PtrToString(entry.Name))
171		if err != nil {
172			return nil, err
173		}
174		sids = append(sids, sid)
175	}
176	return sids, nil
177}
178
179func newUser(uid, gid, dir, username, domain string) (*User, error) {
180	domainAndUser := domain + `\` + username
181	name, e := lookupFullName(domain, username, domainAndUser)
182	if e != nil {
183		return nil, e
184	}
185	u := &User{
186		Uid:      uid,
187		Gid:      gid,
188		Username: domainAndUser,
189		Name:     name,
190		HomeDir:  dir,
191	}
192	return u, nil
193}
194
195var (
196	// unused variables (in this implementation)
197	// modified during test to exercise code paths in the cgo implementation.
198	userBuffer  = 0
199	groupBuffer = 0
200)
201
202func current() (*User, error) {
203	t, e := syscall.OpenCurrentProcessToken()
204	if e != nil {
205		return nil, e
206	}
207	defer t.Close()
208	u, e := t.GetTokenUser()
209	if e != nil {
210		return nil, e
211	}
212	pg, e := t.GetTokenPrimaryGroup()
213	if e != nil {
214		return nil, e
215	}
216	uid, e := u.User.Sid.String()
217	if e != nil {
218		return nil, e
219	}
220	gid, e := pg.PrimaryGroup.String()
221	if e != nil {
222		return nil, e
223	}
224	dir, e := t.GetUserProfileDirectory()
225	if e != nil {
226		return nil, e
227	}
228	username, domain, e := lookupUsernameAndDomain(u.User.Sid)
229	if e != nil {
230		return nil, e
231	}
232	return newUser(uid, gid, dir, username, domain)
233}
234
235// lookupUserPrimaryGroup obtains the primary group SID for a user using this method:
236// https://support.microsoft.com/en-us/help/297951/how-to-use-the-primarygroupid-attribute-to-find-the-primary-group-for
237// The method follows this formula: domainRID + "-" + primaryGroupRID
238func lookupUserPrimaryGroup(username, domain string) (string, error) {
239	// get the domain RID
240	sid, _, t, e := syscall.LookupSID("", domain)
241	if e != nil {
242		return "", e
243	}
244	if t != syscall.SidTypeDomain {
245		return "", fmt.Errorf("lookupUserPrimaryGroup: should be domain account type, not %d", t)
246	}
247	domainRID, e := sid.String()
248	if e != nil {
249		return "", e
250	}
251	// If the user has joined a domain use the RID of the default primary group
252	// called "Domain Users":
253	// https://support.microsoft.com/en-us/help/243330/well-known-security-identifiers-in-windows-operating-systems
254	// SID: S-1-5-21domain-513
255	//
256	// The correct way to obtain the primary group of a domain user is
257	// probing the user primaryGroupID attribute in the server Active Directory:
258	// https://learn.microsoft.com/en-us/windows/win32/adschema/a-primarygroupid
259	//
260	// Note that the primary group of domain users should not be modified
261	// on Windows for performance reasons, even if it's possible to do that.
262	// The .NET Developer's Guide to Directory Services Programming - Page 409
263	// 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
264	joined, err := isDomainJoined()
265	if err == nil && joined {
266		return domainRID + "-513", nil
267	}
268	// For non-domain users call NetUserGetInfo() with level 4, which
269	// in this case would not have any network overhead.
270	// The primary group should not change from RID 513 here either
271	// but the group will be called "None" instead:
272	// https://www.adampalmer.me/iodigitalsec/2013/08/10/windows-null-session-enumeration/
273	// "Group 'None' (RID: 513)"
274	u, e := syscall.UTF16PtrFromString(username)
275	if e != nil {
276		return "", e
277	}
278	d, e := syscall.UTF16PtrFromString(domain)
279	if e != nil {
280		return "", e
281	}
282	var p *byte
283	e = syscall.NetUserGetInfo(d, u, 4, &p)
284	if e != nil {
285		return "", e
286	}
287	defer syscall.NetApiBufferFree(p)
288	i := (*windows.UserInfo4)(unsafe.Pointer(p))
289	return fmt.Sprintf("%s-%d", domainRID, i.PrimaryGroupID), nil
290}
291
292func newUserFromSid(usid *syscall.SID) (*User, error) {
293	username, domain, e := lookupUsernameAndDomain(usid)
294	if e != nil {
295		return nil, e
296	}
297	gid, e := lookupUserPrimaryGroup(username, domain)
298	if e != nil {
299		return nil, e
300	}
301	uid, e := usid.String()
302	if e != nil {
303		return nil, e
304	}
305	// If this user has logged in at least once their home path should be stored
306	// in the registry under the specified SID. References:
307	// https://social.technet.microsoft.com/wiki/contents/articles/13895.how-to-remove-a-corrupted-user-profile-from-the-registry.aspx
308	// https://support.asperasoft.com/hc/en-us/articles/216127438-How-to-delete-Windows-user-profiles
309	//
310	// The registry is the most reliable way to find the home path as the user
311	// might have decided to move it outside of the default location,
312	// (e.g. C:\users). Reference:
313	// 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
314	dir, e := findHomeDirInRegistry(uid)
315	if e != nil {
316		// If the home path does not exist in the registry, the user might
317		// have not logged in yet; fall back to using getProfilesDirectory().
318		// Find the username based on a SID and append that to the result of
319		// getProfilesDirectory(). The domain is not relevant here.
320		dir, e = getProfilesDirectory()
321		if e != nil {
322			return nil, e
323		}
324		dir += `\` + username
325	}
326	return newUser(uid, gid, dir, username, domain)
327}
328
329func lookupUser(username string) (*User, error) {
330	sid, _, t, e := syscall.LookupSID("", username)
331	if e != nil {
332		return nil, e
333	}
334	if t != syscall.SidTypeUser {
335		return nil, fmt.Errorf("user: should be user account type, not %d", t)
336	}
337	return newUserFromSid(sid)
338}
339
340func lookupUserId(uid string) (*User, error) {
341	sid, e := syscall.StringToSid(uid)
342	if e != nil {
343		return nil, e
344	}
345	return newUserFromSid(sid)
346}
347
348func lookupGroup(groupname string) (*Group, error) {
349	sid, err := lookupGroupName(groupname)
350	if err != nil {
351		return nil, err
352	}
353	return &Group{Name: groupname, Gid: sid}, nil
354}
355
356func lookupGroupId(gid string) (*Group, error) {
357	sid, err := syscall.StringToSid(gid)
358	if err != nil {
359		return nil, err
360	}
361	groupname, _, t, err := sid.LookupAccount("")
362	if err != nil {
363		return nil, err
364	}
365	if t != syscall.SidTypeGroup && t != syscall.SidTypeWellKnownGroup && t != syscall.SidTypeAlias {
366		return nil, fmt.Errorf("lookupGroupId: should be group account type, not %d", t)
367	}
368	return &Group{Name: groupname, Gid: gid}, nil
369}
370
371func listGroups(user *User) ([]string, error) {
372	sid, err := syscall.StringToSid(user.Uid)
373	if err != nil {
374		return nil, err
375	}
376	username, domain, err := lookupUsernameAndDomain(sid)
377	if err != nil {
378		return nil, err
379	}
380	sids, err := listGroupsForUsernameAndDomain(username, domain)
381	if err != nil {
382		return nil, err
383	}
384	// Add the primary group of the user to the list if it is not already there.
385	// This is done only to comply with the POSIX concept of a primary group.
386	for _, sid := range sids {
387		if sid == user.Gid {
388			return sids, nil
389		}
390	}
391	return append(sids, user.Gid), nil
392}
393