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://msdn.microsoft.com/en-us/library/cc245478.aspx#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://msdn.microsoft.com/en-us/library/windows/desktop/aa370655(v=vs.85).aspx
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
195func current() (*User, error) {
196	t, e := syscall.OpenCurrentProcessToken()
197	if e != nil {
198		return nil, e
199	}
200	defer t.Close()
201	u, e := t.GetTokenUser()
202	if e != nil {
203		return nil, e
204	}
205	pg, e := t.GetTokenPrimaryGroup()
206	if e != nil {
207		return nil, e
208	}
209	uid, e := u.User.Sid.String()
210	if e != nil {
211		return nil, e
212	}
213	gid, e := pg.PrimaryGroup.String()
214	if e != nil {
215		return nil, e
216	}
217	dir, e := t.GetUserProfileDirectory()
218	if e != nil {
219		return nil, e
220	}
221	username, domain, e := lookupUsernameAndDomain(u.User.Sid)
222	if e != nil {
223		return nil, e
224	}
225	return newUser(uid, gid, dir, username, domain)
226}
227
228// lookupUserPrimaryGroup obtains the primary group SID for a user using this method:
229// https://support.microsoft.com/en-us/help/297951/how-to-use-the-primarygroupid-attribute-to-find-the-primary-group-for
230// The method follows this formula: domainRID + "-" + primaryGroupRID
231func lookupUserPrimaryGroup(username, domain string) (string, error) {
232	// get the domain RID
233	sid, _, t, e := syscall.LookupSID("", domain)
234	if e != nil {
235		return "", e
236	}
237	if t != syscall.SidTypeDomain {
238		return "", fmt.Errorf("lookupUserPrimaryGroup: should be domain account type, not %d", t)
239	}
240	domainRID, e := sid.String()
241	if e != nil {
242		return "", e
243	}
244	// If the user has joined a domain use the RID of the default primary group
245	// called "Domain Users":
246	// https://support.microsoft.com/en-us/help/243330/well-known-security-identifiers-in-windows-operating-systems
247	// SID: S-1-5-21domain-513
248	//
249	// The correct way to obtain the primary group of a domain user is
250	// probing the user primaryGroupID attribute in the server Active Directory:
251	// https://msdn.microsoft.com/en-us/library/ms679375(v=vs.85).aspx
252	//
253	// Note that the primary group of domain users should not be modified
254	// on Windows for performance reasons, even if it's possible to do that.
255	// The .NET Developer's Guide to Directory Services Programming - Page 409
256	// 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
257	joined, err := isDomainJoined()
258	if err == nil && joined {
259		return domainRID + "-513", nil
260	}
261	// For non-domain users call NetUserGetInfo() with level 4, which
262	// in this case would not have any network overhead.
263	// The primary group should not change from RID 513 here either
264	// but the group will be called "None" instead:
265	// https://www.adampalmer.me/iodigitalsec/2013/08/10/windows-null-session-enumeration/
266	// "Group 'None' (RID: 513)"
267	u, e := syscall.UTF16PtrFromString(username)
268	if e != nil {
269		return "", e
270	}
271	d, e := syscall.UTF16PtrFromString(domain)
272	if e != nil {
273		return "", e
274	}
275	var p *byte
276	e = syscall.NetUserGetInfo(d, u, 4, &p)
277	if e != nil {
278		return "", e
279	}
280	defer syscall.NetApiBufferFree(p)
281	i := (*windows.UserInfo4)(unsafe.Pointer(p))
282	return fmt.Sprintf("%s-%d", domainRID, i.PrimaryGroupID), nil
283}
284
285func newUserFromSid(usid *syscall.SID) (*User, error) {
286	username, domain, e := lookupUsernameAndDomain(usid)
287	if e != nil {
288		return nil, e
289	}
290	gid, e := lookupUserPrimaryGroup(username, domain)
291	if e != nil {
292		return nil, e
293	}
294	uid, e := usid.String()
295	if e != nil {
296		return nil, e
297	}
298	// If this user has logged in at least once their home path should be stored
299	// in the registry under the specified SID. References:
300	// https://social.technet.microsoft.com/wiki/contents/articles/13895.how-to-remove-a-corrupted-user-profile-from-the-registry.aspx
301	// https://support.asperasoft.com/hc/en-us/articles/216127438-How-to-delete-Windows-user-profiles
302	//
303	// The registry is the most reliable way to find the home path as the user
304	// might have decided to move it outside of the default location,
305	// (e.g. C:\users). Reference:
306	// 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
307	dir, e := findHomeDirInRegistry(uid)
308	if e != nil {
309		// If the home path does not exist in the registry, the user might
310		// have not logged in yet; fall back to using getProfilesDirectory().
311		// Find the username based on a SID and append that to the result of
312		// getProfilesDirectory(). The domain is not relevant here.
313		dir, e = getProfilesDirectory()
314		if e != nil {
315			return nil, e
316		}
317		dir += `\` + username
318	}
319	return newUser(uid, gid, dir, username, domain)
320}
321
322func lookupUser(username string) (*User, error) {
323	sid, _, t, e := syscall.LookupSID("", username)
324	if e != nil {
325		return nil, e
326	}
327	if t != syscall.SidTypeUser {
328		return nil, fmt.Errorf("user: should be user account type, not %d", t)
329	}
330	return newUserFromSid(sid)
331}
332
333func lookupUserId(uid string) (*User, error) {
334	sid, e := syscall.StringToSid(uid)
335	if e != nil {
336		return nil, e
337	}
338	return newUserFromSid(sid)
339}
340
341func lookupGroup(groupname string) (*Group, error) {
342	sid, err := lookupGroupName(groupname)
343	if err != nil {
344		return nil, err
345	}
346	return &Group{Name: groupname, Gid: sid}, nil
347}
348
349func lookupGroupId(gid string) (*Group, error) {
350	sid, err := syscall.StringToSid(gid)
351	if err != nil {
352		return nil, err
353	}
354	groupname, _, t, err := sid.LookupAccount("")
355	if err != nil {
356		return nil, err
357	}
358	if t != syscall.SidTypeGroup && t != syscall.SidTypeWellKnownGroup && t != syscall.SidTypeAlias {
359		return nil, fmt.Errorf("lookupGroupId: should be group account type, not %d", t)
360	}
361	return &Group{Name: groupname, Gid: gid}, nil
362}
363
364func listGroups(user *User) ([]string, error) {
365	sid, err := syscall.StringToSid(user.Uid)
366	if err != nil {
367		return nil, err
368	}
369	username, domain, err := lookupUsernameAndDomain(sid)
370	if err != nil {
371		return nil, err
372	}
373	sids, err := listGroupsForUsernameAndDomain(username, domain)
374	if err != nil {
375		return nil, err
376	}
377	// Add the primary group of the user to the list if it is not already there.
378	// This is done only to comply with the POSIX concept of a primary group.
379	for _, sid := range sids {
380		if sid == user.Gid {
381			return sids, nil
382		}
383	}
384	return append(sids, user.Gid), nil
385}
386