Files
nomad/helper/winsvc/path_windows.go
Chris Roberts 48d91dc1f9 [winsvc] Add interfaces for Windows services and service manager
Provides interfaces to the Windows service manager and Windows
services. These interfaces support creating new Windows services,
deleting Windows services, configuring Windows services, and
registering/deregistering services with Windows Eventlog.

A path helper is included to support expansion of paths using a
subset of known folder IDs.

A privileged helper is included to check that the process is
currently being executed with elevated privileges, which are
required for managing Windows services and modifying the registry.
2025-09-02 16:39:45 -07:00

207 lines
5.2 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package winsvc
import (
"bytes"
"errors"
"fmt"
"os"
"strings"
"sync"
"text/template"
"golang.org/x/sys/windows"
"golang.org/x/sys/windows/registry"
)
func NewWindowsPaths() WindowsPaths {
return &windowsPaths{}
}
type windowsPaths struct {
SystemRoot string
SystemDrive string
ProgramData string
ProgramFiles string
loadErr error
o sync.Once
}
func (w *windowsPaths) Expand(path string) (string, error) {
if err := w.load(); err != nil {
return "", err
}
tmpl := template.New("expansion").Option("missingkey=error")
tmpl, err := tmpl.Parse(path)
if err != nil {
return "", err
}
result := new(bytes.Buffer)
if err := tmpl.Execute(result, w); err != nil {
return "", err
}
return result.String(), nil
}
func (w *windowsPaths) CreateDirectory(path string, restrict_on_create bool) error {
s, err := os.Stat(path)
if err != nil && !errors.Is(err, os.ErrNotExist) {
return err
}
if err == nil {
// Directory exists so nothing to do
if s.IsDir() {
return nil
}
return fmt.Errorf("path exists and is not directory - %s", path)
}
// NOTE: mode ignored on Windows. If directory should
// be restricted, an ACL will be applied below.
if err := os.MkdirAll(path, 0o000); err != nil {
return err
}
// Since the directory was just created, apply access
// restrictions if requested
if restrict_on_create {
if err := setDirectoryPermissions(path); err != nil {
return err
}
}
return nil
}
func getUserGroupSIDs() (usid *windows.SID, gsid *windows.SID, err error) {
// NOTE: this token is a pseudo-token and does not
// need to be closed
token := windows.GetCurrentProcessToken()
userToken, err := token.GetTokenUser()
if err != nil {
return
}
usid = userToken.User.Sid
userGroup, err := token.GetTokenPrimaryGroup()
if err != nil {
return
}
gsid = userGroup.PrimaryGroup
return
}
func setDirectoryPermissions(path string) error {
// Grab the user and group SID for who is running the process
userSid, groupSid, err := getUserGroupSIDs()
if err != nil {
return err
}
// Generate a SID for the administators group
gsid, err := windows.CreateWellKnownSid(windows.WinBuiltinAdministratorsSid)
if err != nil {
return err
}
// Create an ACL with an ACE for user SID and an ACE for the
// administrators group SID, both of which are granted full
// access. No other ACEs are provided which restricts access
// from non-administrators
dacl, err := windows.ACLFromEntries(
[]windows.EXPLICIT_ACCESS{
{
AccessPermissions: windows.GENERIC_ALL,
AccessMode: windows.SET_ACCESS,
Inheritance: windows.SUB_CONTAINERS_AND_OBJECTS_INHERIT,
Trustee: windows.TRUSTEE{
MultipleTrusteeOperation: windows.NO_MULTIPLE_TRUSTEE,
TrusteeForm: windows.TRUSTEE_IS_SID,
TrusteeType: windows.TRUSTEE_IS_USER,
TrusteeValue: windows.TrusteeValueFromSID(userSid),
},
},
{
AccessPermissions: windows.GENERIC_ALL,
AccessMode: windows.SET_ACCESS,
Inheritance: windows.SUB_CONTAINERS_AND_OBJECTS_INHERIT,
Trustee: windows.TRUSTEE{
MultipleTrusteeOperation: windows.NO_MULTIPLE_TRUSTEE,
TrusteeForm: windows.TRUSTEE_IS_SID,
TrusteeType: windows.TRUSTEE_IS_WELL_KNOWN_GROUP,
TrusteeValue: windows.TrusteeValueFromSID(gsid),
},
},
}, nil,
)
if err != nil {
return err
}
// Apply the ACL to the directory
if err := windows.SetNamedSecurityInfo(path, windows.SE_FILE_OBJECT,
windows.OWNER_SECURITY_INFORMATION|
windows.GROUP_SECURITY_INFORMATION|
windows.DACL_SECURITY_INFORMATION|
windows.PROTECTED_DACL_SECURITY_INFORMATION,
userSid, groupSid, dacl, nil); err != nil {
return err
}
return nil
}
func (w *windowsPaths) load() error {
w.o.Do(func() {
w.SystemDrive = os.Getenv("SystemDrive")
if w.SystemDrive == "" {
w.loadErr = fmt.Errorf("cannot detect Windows SystemDrive path")
return
}
w.SystemRoot = strings.ReplaceAll(os.Getenv("SystemDrive"), "SystemDrive", w.SystemDrive)
w.ProgramData = os.Getenv("ProgramData")
if w.ProgramData == "" {
pdKey, err := registry.OpenKey(registry.LOCAL_MACHINE,
`SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList`, registry.QUERY_VALUE)
if err == nil {
if pdVal, _, err := pdKey.GetStringValue("ProgramData"); err == nil {
w.ProgramData = pdVal
}
}
}
if w.ProgramData == "" {
w.loadErr = fmt.Errorf("cannot detect Windows ProgramData path")
return
}
w.ProgramData = strings.ReplaceAll(w.ProgramData, "SystemDrive", w.SystemDrive)
w.ProgramFiles = os.Getenv("ProgramFiles")
if w.ProgramFiles == "" {
pdKey, err := registry.OpenKey(registry.LOCAL_MACHINE,
`SOFTWARE\Microsoft\Windows\CurrentVersion`, registry.QUERY_VALUE)
if err == nil {
if pdVal, _, err := pdKey.GetStringValue("ProgramFilesDir"); err == nil {
w.ProgramFiles = pdVal
}
}
}
if w.ProgramFiles == "" {
w.loadErr = fmt.Errorf("cannot detect Windows ProgramFiles path")
return
}
w.ProgramFiles = strings.ReplaceAll(w.ProgramFiles, "SystemDrive", w.SystemDrive)
})
return w.loadErr
}