package main // The decisions behind the Windows drop (freeagent_drop_windows.go), kept in an // untagged file so the suite checks them on every platform. The Windows calls // that gather their inputs are in the tagged file. import ( "sort" "strconv" "strings" "unicode/utf16" ) // winSidTypeUser is SidTypeUser: the SID names a person's account rather than // a group, an alias or a well-known identity. const winSidTypeUser = 1 // faWinOwnerRefusal says why a Windows account may NOT run a folder's agent, // or "" when it may. It is the Windows reading of the unix rule (root, nobody, // no account): the agent runs as the person who owns the folder, never as one // of the machine's own identities, a group, or an account anyone can use. // // - SYSTEM, LOCAL SERVICE, NETWORK SERVICE, the service and virtual accounts // (S-1-5-80, -82, -83, -90, -96) and the BUILTIN groups (S-1-5-32, which is // where Administrators is, and the usual owner of a folder an elevated // administrator made) are the machine's identities, not a person. // - Only a user account issued by the machine or its domain (S-1-5-21-) or by // Microsoft Entra ID (S-1-12-1-) is a person. // - The built-in Administrator (RID 500) runs with full rights and no UAC // split by default, so an agent under its token would be the machine's // administrator: the root case. The built-in Guest (RID 501) is shared. // Every RID below 1000 is reserved for a built-in account the machine or // its domain made (krbtgt 502, DefaultAccount 503, WDAGUtilityAccount 504, // and the rest); a person's account is issued a RID of 1000 or more. None // of the reserved ones is a person, so all are refused. func faWinOwnerRefusal(sid string, sidType uint32, name string) string { grant := " — grant a folder owned by the user who should read it" switch sid { case "S-1-5-18": return "refusing to run the agent as SYSTEM: the granted folder is owned by the system" + grant case "S-1-5-19", "S-1-5-20": return "refusing to run the agent as a service account: the granted folder is owned by " + name + grant } for _, p := range []string{"S-1-5-32-", "S-1-5-80-", "S-1-5-82-", "S-1-5-83-", "S-1-5-90-", "S-1-5-96-"} { if strings.HasPrefix(sid, p) { return "refusing to run the agent as " + name + ": the granted folder is owned by one of the machine's own identities, not a person" + grant } } person := strings.HasPrefix(sid, "S-1-5-21-") || strings.HasPrefix(sid, "S-1-12-1-") if !person || sidType != winSidTypeUser { return "refusing to run the agent as " + name + ": the granted folder's owner is not a person's account" + grant } if strings.HasPrefix(sid, "S-1-5-21-") { if rid, err := strconv.Atoi(sid[strings.LastIndex(sid, "-")+1:]); err == nil && rid < 1000 { switch rid { case 500: return "refusing to run the agent as the built-in Administrator: it runs with the machine's full rights" + grant case 501: return "refusing to run the agent as the built-in Guest: anyone may use it" + grant default: return "refusing to run the agent as " + name + ": the granted folder is owned by a built-in account, not a person" + grant } } } return "" } // winJoin joins Windows path parts with a backslash, whatever platform this // runs on, so the environment below is tested off Windows. func winJoin(parts ...string) string { out := strings.TrimRight(parts[0], `\`) for _, p := range parts[1:] { out += `\` + strings.Trim(p, `\`) } return out } // faWinDropEnv is what a Windows child is told about who it is, and which of // the service's variables it keeps. The service runs as LocalSystem, so its // USERPROFILE, APPDATA, LOCALAPPDATA and TEMP all name LocalSystem's profile, // which the owner cannot open; they are replaced with the owner's own. A // Python child on Windows also needs SystemRoot to open a socket, so that is // kept, with the other machine-wide variables that say nothing about the // account. func faWinDropEnv(user, profile, stateDir string) (set []string, passthrough []string) { home := profile temp := stateDir if profile != "" { temp = winJoin(profile, `AppData\Local\Temp`) } else { home = stateDir } set = []string{"USERNAME=" + user, "HOME=" + home, "USERPROFILE=" + home, "TEMP=" + temp, "TMP=" + temp} if profile != "" { set = append(set, "APPDATA="+winJoin(profile, `AppData\Roaming`), "LOCALAPPDATA="+winJoin(profile, `AppData\Local`)) } passthrough = []string{"PATH", "SystemRoot", "SYSTEMROOT", "windir", "WINDIR", "ComSpec", "PATHEXT", "PROCESSOR_ARCHITECTURE", "NUMBER_OF_PROCESSORS", "LANG", "LC_ALL"} return set, passthrough } // faWinEnvBlock is the environment block CreateProcessAsUser takes: each // NAME=value NUL-terminated, the block ended by one more NUL, sorted by name // without regard to case as Windows requires. A later duplicate of a name // (in any case) wins, and the drive-letter entries that begin with '=' are // left out. func faWinEnvBlock(env []string) []uint16 { byName := map[string]string{} for _, kv := range env { i := strings.IndexByte(kv, '=') if i <= 0 { continue } byName[strings.ToUpper(kv[:i])] = kv } keys := make([]string, 0, len(byName)) for k := range byName { keys = append(keys, k) } sort.Strings(keys) var out []uint16 for _, k := range keys { out = append(out, utf16.Encode([]rune(byName[k]))...) out = append(out, 0) } if len(out) == 0 { out = append(out, 0) } return append(out, 0) }