Windows: check user's system privileges when Shaman is enabled

Symbolic links on Windows require some special user privilege, and
Shaman can now check for this at startup. Hopefully this helps in guiding
people towards a working Shaman system.
This commit is contained in:
Sybren A. Stüvel 2023-10-16 21:36:16 +02:00
parent 40f76efec9
commit aa4d3cff75
3 changed files with 121 additions and 23 deletions

@ -109,7 +109,8 @@ func checkPlatformSymlinkSupport() {
Str("os", runtime.GOOS).
Str("arch", runtime.GOARCH).
Str("osDetail", osDetail).
Msg("this platform does not reliably support symbolic links; using the Shaman system is not recommended")
Msg("this platform does not reliably support symbolic links, " +
"see https://flamenco.blender.org/usage/shared-storage/shaman/#requirements")
}
// Go starts goroutines for background operations.

@ -3,34 +3,23 @@ package sysinfo
// SPDX-License-Identifier: GPL-3.0-or-later
import (
"encoding/binary"
"fmt"
"syscall"
"unsafe"
"github.com/rs/zerolog/log"
"golang.org/x/sys/windows"
"golang.org/x/sys/windows/registry"
)
// editionCanSymlink maps the EditionID key in the registry to a boolean indicating whether
// symlinks are available.
var editionCanSymlink = map[string]bool{
"Core": false, // Windows Home misses the tool to allow symlinking to a user.
"Professional": true, // Still requires explicit right to be assigned to the user.
}
// canSymlink tries to determine whether the running system can use symbolic
// links, based on information from the Windows registry.
func canSymlink() (bool, error) {
editionID, err := windowsEditionID()
if err != nil {
return false, fmt.Errorf("determining edition of Windows: %w", err)
}
canSymlink, found := editionCanSymlink[editionID]
if !found {
log.Warn().Str("editionID", editionID).Msg("unknown Windows edition, assuming it can use symlinks")
if isDeveloperModeActive() {
return true, nil
}
return canSymlink, nil
return hasSystemPrivilege("SeCreateSymbolicLinkPrivilege")
}
func description() (string, error) {
@ -78,3 +67,87 @@ func registryReadString(keyPath, valueName string) (string, error) {
return value, nil
}
// isDeveloperModeActive checks whether or not the developer mode is active on Windows 10.
// Returns false for prior Windows versions.
// see https://docs.microsoft.com/en-us/windows/uwp/get-started/enable-your-device-for-development
// Copied from https://github.com/golang/go/pull/24307/files
func isDeveloperModeActive() bool {
key, err := registry.OpenKey(
registry.LOCAL_MACHINE,
`SOFTWARE\Microsoft\Windows\CurrentVersion\AppModelUnlock`,
registry.READ)
if err != nil {
return false
}
val, _, err := key.GetIntegerValue("AllowDevelopmentWithoutDevLicense")
if err != nil {
return false
}
return val != 0
}
// hasSystemPrivilege checks whether the user has the
// SeCreateSymbolicLinkPrivilege, which is necessary to create symbolic links.
func hasSystemPrivilege(privilegeName string) (bool, error) {
// This doesn't fail, and just returns -1. The Microsoft docs still recommend
// calling this function, though, instead of just hard-coding the -1 value.
hProcess := windows.CurrentProcess()
// Open a process token, necessary for the subsequent calls.
var processToken windows.Token
err := windows.OpenProcessToken(hProcess, windows.TOKEN_READ, &processToken)
if err != nil {
return false, fmt.Errorf("calling OpenProcessToken: %w", err)
}
defer func() {
_ = processToken.Close()
}()
privilegeNameU16, err := syscall.UTF16PtrFromString(privilegeName)
if err != nil {
return false, fmt.Errorf("invalid privilege name %q: %w", privilegeName, err)
}
// Look up the LUID for the privilege.
var privilegeLUID windows.LUID
err = windows.LookupPrivilegeValue(nil, privilegeNameU16, &privilegeLUID)
if err != nil {
return false, fmt.Errorf("calling LookupPrivilegeValue: %w", err)
}
// Get the size of the buffer needed for the actual data.
var TokenPrivileges uint32 = 3
var bufferSize uint32
err = windows.GetTokenInformation(processToken, TokenPrivileges, nil, 0, &bufferSize)
if errno, ok := err.(syscall.Errno); !ok || errno != 122 {
return false, fmt.Errorf("unexpected error from first GetTokenInformation call: %w", err)
}
// Get the list of user's privileges.
buffer := make([]byte, bufferSize)
err = windows.GetTokenInformation(processToken, TokenPrivileges, &buffer[0], bufferSize, &bufferSize)
if err != nil {
return false, fmt.Errorf("unexpected error from second GetTokenInformation call: %w", err)
}
// Decode the privileges.
privCount := int(binary.LittleEndian.Uint32(buffer))
offset := int(unsafe.Sizeof(uint32(0)))
structSize := int(unsafe.Sizeof(windows.LUIDAndAttributes{}))
for i := 0; i < privCount; i++ {
structPtr := &buffer[offset+structSize*i]
luidAndAttr := *(*windows.LUIDAndAttributes)(unsafe.Pointer(structPtr))
if luidAndAttr.Luid != privilegeLUID {
continue
}
log.Debug().Str("privilege", privilegeName).Msg("found privilege")
return true, nil
}
return false, nil
}

@ -53,13 +53,33 @@ Because of the use of *symbolic links* (also known as *symlinks*), using Shaman
is only possible on systems that support those. These should be supported by the
computers running Flamenco Manager and Workers.
### Windows
The Shaman storage system uses _symbolic links_. On Windows the creation of
symbolic links requires a change in security policy. Unfortunately, *Home*
editions of Windows do not have a policy editor, but the freely available
[Polsedit][polsedit] can be used on these editions.
symbolic links requires a change in security policy. This can be done as
follows:
{{< tabs "shaman-windows" >}}
{{< tab "Windows Home / Core" >}}
On Windows Home (also known as "core"), you'll need to enable Developer Mode:
1. Press the Windows key, type "*Developer settings*", and click Open or press
Enter.
2. Click the slider under "*Developer Mode*" to turn it ON.
See [Developer Mode][devmode] for more information, including some security implications.
Alternatively you can use the freely available [Polsedit][polsedit] to enable
the *Create Symbolic Links* security policy.
[devmode]: https://learn.microsoft.com/en-us/windows/apps/get-started/enable-your-device-for-development
[polsedit]: https://www.southsoftware.com/polsedit.html
{{< /tab >}}
{{< tab "Windows Pro / Enterprise" >}}
On Windows Pro & Enterprise you need to enable a security policy.
1. Press Win+R, in the popup type `secpol.msc`. Then click OK.
2. In the _Local Security Policy_ window that opens, go to _Security Settings_ > _Local Policies_ > _User Rights Assignment_.
@ -67,8 +87,12 @@ editions of Windows do not have a policy editor, but the freely available
4. Double-click the item and add yourself (or the user running Flamenco Manager or the whole users group) to the list.
5. Log out & back in again, or reboot the machine.
[polsedit]: https://www.southsoftware.com/polsedit.html
For more info see [the Microsoft documentation][secpol].
[secpol]: https://learn.microsoft.com/en-us/windows/security/threat-protection/security-policy-settings/create-symbolic-links
{{< /tab >}}
{{< /tabs >}}
### Linux