mirror of
https://github.com/tailscale/tailscale.git
synced 2024-11-25 19:15:34 +00:00
80b2b45d60
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>
80 lines
2.4 KiB
Go
80 lines
2.4 KiB
Go
// 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"
|
|
)
|
|
|
|
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
|
|
}
|
|
|
|
func (pm *profileManager) loadLegacyPrefs(uid ipn.WindowsUserID) (string, ipn.PrefsView, error) {
|
|
userLegacyPrefsDir, err := legacyPrefsDir(uid)
|
|
if err != nil {
|
|
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)
|
|
}
|