tailscale/ipn/ipnlocal/profiles_windows.go

80 lines
2.4 KiB
Go
Raw Normal View History

// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package ipnlocal
import (
"errors"
"fmt"
"io/fs"
"os"
"os/user"
"path/filepath"
"tailscale.com/atomicfile"
"tailscale.com/ipn"
"tailscale.com/util/winutil/policy"
)
const (
legacyPrefsFile = "prefs"
legacyPrefsMigrationSentinelFile = "_migrated-to-profiles"
legacyPrefsExt = ".conf"
)
ipn/ipnlocal: refactor and cleanup profileManager In preparation for multi-user and unattended mode improvements, we are refactoring and cleaning up `ipn/ipnlocal.profileManager`. The concept of the "current user", which is only relevant on Windows, is being deprecated and will soon be removed to allow more than one Windows user to connect and utilize `LocalBackend` according to that user's access rights to the device and specific Tailscale profiles. We plan to pass the user's identity down to the `profileManager`, where it can be used to determine the user's access rights to a given `LoginProfile`. While the new permission model in `ipnauth` requires more work and is currently blocked pending PR reviews, we are updating the `profileManager` to reduce its reliance on the concept of a single OS user being connected to the backend at the same time. We extract the switching to the default Tailscale profile, which may also trigger legacy profile migration, from `profileManager.SetCurrentUserID`. This introduces `profileManager.DefaultUserProfileID`, which returns the default profile ID for the current user, and `profileManager.SwitchToDefaultProfile`, which is essentially a shorthand for `pm.SwitchProfile(pm.DefaultUserProfileID())`. Both methods will eventually be updated to accept the user's identity and utilize that user's default profile. We make access checks more explicit by introducing the `profileManager.checkProfileAccess` method. The current implementation continues to use `profileManager.currentUserID` and `LoginProfile.LocalUserID` to determine whether access to a given profile should be granted. This will be updated to utilize the `ipnauth` package and the new permissions model once it's ready. We also expand access checks to be used more widely in the `profileManager`, not just when switching or listing profiles. This includes access checks in methods like `SetPrefs` and, most notably, `DeleteProfile` and `DeleteAllProfiles`, preventing unprivileged Windows users from deleting Tailscale profiles owned by other users on the same device, including profiles owned by local admins. We extract `profileManager.ProfilePrefs` and `profileManager.SetProfilePrefs` methods that can be used to get and set preferences of a given `LoginProfile` if `profileManager.checkProfileAccess` permits access to it. We also update `profileManager.setUnattendedModeAsConfigured` to always enable unattended mode on Windows if `Prefs.ForceDaemon` is true in the current `LoginProfile`, even if `profileManager.currentUserID` is `""`. This facilitates enabling unattended mode via `tailscale up --unattended` even if `tailscale-ipn.exe` is not running, such as when a Group Policy or MDM-deployed script runs at boot time, or when Tailscale is used on a Server Code or otherwise headless Windows environments. See #12239, #2137, #3186 and https://github.com/tailscale/tailscale/pull/6255#issuecomment-2016623838 for details. Fixes #12239 Updates tailscale/corp#18342 Updates #3186 Updates #2137 Signed-off-by: Nick Khyl <nickk@tailscale.com>
2024-08-28 19:23:35 +00:00
var errAlreadyMigrated = errors.New("profile migration already completed")
func legacyPrefsDir(uid ipn.WindowsUserID) (string, error) {
// TODO(aaron): Ideally we'd have the impersonation token for the pipe's
// client and use it to call SHGetKnownFolderPath, thus yielding the correct
// path without having to make gross assumptions about directory names.
usr, err := user.LookupId(string(uid))
if err != nil {
return "", err
}
if usr.HomeDir == "" {
return "", fmt.Errorf("user %q does not have a home directory", uid)
}
userLegacyPrefsDir := filepath.Join(usr.HomeDir, "AppData", "Local", "Tailscale")
return userLegacyPrefsDir, nil
}
ipn/ipnlocal: refactor and cleanup profileManager In preparation for multi-user and unattended mode improvements, we are refactoring and cleaning up `ipn/ipnlocal.profileManager`. The concept of the "current user", which is only relevant on Windows, is being deprecated and will soon be removed to allow more than one Windows user to connect and utilize `LocalBackend` according to that user's access rights to the device and specific Tailscale profiles. We plan to pass the user's identity down to the `profileManager`, where it can be used to determine the user's access rights to a given `LoginProfile`. While the new permission model in `ipnauth` requires more work and is currently blocked pending PR reviews, we are updating the `profileManager` to reduce its reliance on the concept of a single OS user being connected to the backend at the same time. We extract the switching to the default Tailscale profile, which may also trigger legacy profile migration, from `profileManager.SetCurrentUserID`. This introduces `profileManager.DefaultUserProfileID`, which returns the default profile ID for the current user, and `profileManager.SwitchToDefaultProfile`, which is essentially a shorthand for `pm.SwitchProfile(pm.DefaultUserProfileID())`. Both methods will eventually be updated to accept the user's identity and utilize that user's default profile. We make access checks more explicit by introducing the `profileManager.checkProfileAccess` method. The current implementation continues to use `profileManager.currentUserID` and `LoginProfile.LocalUserID` to determine whether access to a given profile should be granted. This will be updated to utilize the `ipnauth` package and the new permissions model once it's ready. We also expand access checks to be used more widely in the `profileManager`, not just when switching or listing profiles. This includes access checks in methods like `SetPrefs` and, most notably, `DeleteProfile` and `DeleteAllProfiles`, preventing unprivileged Windows users from deleting Tailscale profiles owned by other users on the same device, including profiles owned by local admins. We extract `profileManager.ProfilePrefs` and `profileManager.SetProfilePrefs` methods that can be used to get and set preferences of a given `LoginProfile` if `profileManager.checkProfileAccess` permits access to it. We also update `profileManager.setUnattendedModeAsConfigured` to always enable unattended mode on Windows if `Prefs.ForceDaemon` is true in the current `LoginProfile`, even if `profileManager.currentUserID` is `""`. This facilitates enabling unattended mode via `tailscale up --unattended` even if `tailscale-ipn.exe` is not running, such as when a Group Policy or MDM-deployed script runs at boot time, or when Tailscale is used on a Server Code or otherwise headless Windows environments. See #12239, #2137, #3186 and https://github.com/tailscale/tailscale/pull/6255#issuecomment-2016623838 for details. Fixes #12239 Updates tailscale/corp#18342 Updates #3186 Updates #2137 Signed-off-by: Nick Khyl <nickk@tailscale.com>
2024-08-28 19:23:35 +00:00
func (pm *profileManager) loadLegacyPrefs(uid ipn.WindowsUserID) (string, ipn.PrefsView, error) {
userLegacyPrefsDir, err := legacyPrefsDir(uid)
if err != nil {
ipn/ipnlocal: refactor and cleanup profileManager In preparation for multi-user and unattended mode improvements, we are refactoring and cleaning up `ipn/ipnlocal.profileManager`. The concept of the "current user", which is only relevant on Windows, is being deprecated and will soon be removed to allow more than one Windows user to connect and utilize `LocalBackend` according to that user's access rights to the device and specific Tailscale profiles. We plan to pass the user's identity down to the `profileManager`, where it can be used to determine the user's access rights to a given `LoginProfile`. While the new permission model in `ipnauth` requires more work and is currently blocked pending PR reviews, we are updating the `profileManager` to reduce its reliance on the concept of a single OS user being connected to the backend at the same time. We extract the switching to the default Tailscale profile, which may also trigger legacy profile migration, from `profileManager.SetCurrentUserID`. This introduces `profileManager.DefaultUserProfileID`, which returns the default profile ID for the current user, and `profileManager.SwitchToDefaultProfile`, which is essentially a shorthand for `pm.SwitchProfile(pm.DefaultUserProfileID())`. Both methods will eventually be updated to accept the user's identity and utilize that user's default profile. We make access checks more explicit by introducing the `profileManager.checkProfileAccess` method. The current implementation continues to use `profileManager.currentUserID` and `LoginProfile.LocalUserID` to determine whether access to a given profile should be granted. This will be updated to utilize the `ipnauth` package and the new permissions model once it's ready. We also expand access checks to be used more widely in the `profileManager`, not just when switching or listing profiles. This includes access checks in methods like `SetPrefs` and, most notably, `DeleteProfile` and `DeleteAllProfiles`, preventing unprivileged Windows users from deleting Tailscale profiles owned by other users on the same device, including profiles owned by local admins. We extract `profileManager.ProfilePrefs` and `profileManager.SetProfilePrefs` methods that can be used to get and set preferences of a given `LoginProfile` if `profileManager.checkProfileAccess` permits access to it. We also update `profileManager.setUnattendedModeAsConfigured` to always enable unattended mode on Windows if `Prefs.ForceDaemon` is true in the current `LoginProfile`, even if `profileManager.currentUserID` is `""`. This facilitates enabling unattended mode via `tailscale up --unattended` even if `tailscale-ipn.exe` is not running, such as when a Group Policy or MDM-deployed script runs at boot time, or when Tailscale is used on a Server Code or otherwise headless Windows environments. See #12239, #2137, #3186 and https://github.com/tailscale/tailscale/pull/6255#issuecomment-2016623838 for details. Fixes #12239 Updates tailscale/corp#18342 Updates #3186 Updates #2137 Signed-off-by: Nick Khyl <nickk@tailscale.com>
2024-08-28 19:23:35 +00:00
pm.dlogf("no legacy preferences directory for %q: %v", uid, err)
return "", ipn.PrefsView{}, err
}
migrationSentinel := filepath.Join(userLegacyPrefsDir, legacyPrefsMigrationSentinelFile+legacyPrefsExt)
// verify that migration sentinel is not present
_, err = os.Stat(migrationSentinel)
if err == nil {
pm.dlogf("migration sentinel %q already exists", migrationSentinel)
return "", ipn.PrefsView{}, errAlreadyMigrated
}
if !os.IsNotExist(err) {
pm.dlogf("os.Stat(%q) = %v", migrationSentinel, err)
return "", ipn.PrefsView{}, err
}
prefsPath := filepath.Join(userLegacyPrefsDir, legacyPrefsFile+legacyPrefsExt)
prefs, err := ipn.LoadPrefsWindows(prefsPath)
pm.dlogf("ipn.LoadPrefs(%q) = %v, %v", prefsPath, prefs, err)
if errors.Is(err, fs.ErrNotExist) {
return "", ipn.PrefsView{}, errAlreadyMigrated
}
if err != nil {
return "", ipn.PrefsView{}, err
}
prefs.ControlURL = policy.SelectControlURL(defaultPrefs.ControlURL(), prefs.ControlURL)
pm.logf("migrating Windows profile to new format")
return migrationSentinel, prefs.View(), nil
}
func (pm *profileManager) completeMigration(migrationSentinel string) {
atomicfile.WriteFile(migrationSentinel, []byte{}, 0600)
}