Merge branch 'main' into embedded-vnc

This commit is contained in:
Viktor Liu
2026-09-02 19:10:03 +02:00
153 changed files with 9089 additions and 1934 deletions
+98 -1
View File
@@ -70,6 +70,7 @@ type ConfigInput struct {
StateFilePath string
PreSharedKey *string
ServerSSHAllowed *bool
RemoteJobsAllowed *bool
ServerVNCAllowed *bool
DisableVNCApproval *bool
EnableSSHRoot *bool
@@ -129,6 +130,7 @@ type Config struct {
RosenpassEnabled bool
RosenpassPermissive bool
ServerSSHAllowed *bool
RemoteJobsAllowed *bool
ServerVNCAllowed *bool
DisableVNCApproval *bool
EnableSSHRoot *bool
@@ -196,6 +198,12 @@ type Config struct {
// Runtime-only: re-derived from MDM policy on each load, never persisted.
LazyConnection string `json:"-"`
// DebugBundleUploadURL is the MDM-managed debug-bundle upload URL override.
// When set, it takes precedence over the management-supplied upload URL for
// remote debug bundle jobs. Runtime-only: re-derived from MDM policy on each
// load, never persisted.
DebugBundleUploadURL string `json:"-"`
MTU uint16
// policy is the MDM policy that produced the currently-set values for
@@ -229,6 +237,12 @@ func getConfigDir() (string, error) {
}
configDir := filepath.Join(base, "netbird")
// Under sudo this is the invoking user's directory and strictly read-only:
// anything root creates in it would be root-owned and break the user's own
// runs. Reads of a missing directory fall through to defaults.
if sudoActive() {
return configDir, nil
}
if err := os.MkdirAll(configDir, 0o755); err != nil {
return "", err
}
@@ -236,6 +250,16 @@ func getConfigDir() (string, error) {
}
func baseConfigDir() (string, error) {
if u, ok := sudoInvokingUser(); ok {
return userBaseConfigDir(u)
}
// Fail closed instead of falling through to root's own config directory:
// reading root's active-profile and email state for what is actually the
// invoking user's invocation is the very confusion this resolution exists
// to prevent.
if sudoActive() {
return "", fmt.Errorf("resolve sudo invoking user %q: refusing to fall back to root's config directory", os.Getenv(envSudoUser))
}
if runtime.GOOS == "darwin" {
if u, err := user.Current(); err == nil && u.HomeDir != "" {
return filepath.Join(u.HomeDir, "Library", "Application Support"), nil
@@ -277,7 +301,10 @@ func createNewConfig(input ConfigInput) (*Config, error) {
config := &Config{
// defaults to false only for new (post 0.26) configurations
ServerSSHAllowed: util.False(),
WgPort: iface.DefaultWgPort,
// Remote jobs are an explicit opt-in and default off, including for
// legacy configs (a nil value materializes to false at connect time).
RemoteJobsAllowed: util.False(),
WgPort: iface.DefaultWgPort,
}
if _, err := config.apply(input); err != nil {
@@ -507,6 +534,21 @@ func (config *Config) apply(input ConfigInput) (updated bool, err error) {
}
}
if input.RemoteJobsAllowed != nil && (config.RemoteJobsAllowed == nil || *input.RemoteJobsAllowed != *config.RemoteJobsAllowed) {
if *input.RemoteJobsAllowed {
log.Infof("enabling remote jobs")
} else {
log.Infof("disabling remote jobs")
}
config.RemoteJobsAllowed = input.RemoteJobsAllowed
updated = true
} else if config.RemoteJobsAllowed == nil {
// Remote jobs are an explicit opt-in: unlike SSH, a pre-existing config
// with no value defaults to disabled rather than being turned on.
config.RemoteJobsAllowed = util.False()
updated = true
}
if input.EnableSSHRoot != nil && (config.EnableSSHRoot == nil || *input.EnableSSHRoot != *config.EnableSSHRoot) {
if *input.EnableSSHRoot {
log.Infof("enabling SSH root login")
@@ -716,6 +758,14 @@ func (config *Config) apply(input ConfigInput) (updated bool, err error) {
// for the key, so per-field rejection of user writes still applies).
func (config *Config) applyMDMPolicy(policy *mdm.Policy) {
config.policy = policy
// DebugBundleUploadURL is a runtime-only override re-derived from MDM on
// every apply. Resolve it unconditionally (before the IsEmpty early return)
// so a policy that drops the key, becomes empty, or carries an invalid
// value can never leave a previously-enforced upload target active on a
// reused Config instance.
config.DebugBundleUploadURL = mdmDebugBundleUploadURL(policy)
if policy.IsEmpty() {
return
}
@@ -763,6 +813,7 @@ func (config *Config) applyMDMPolicy(policy *mdm.Policy) {
}
applyBool(mdm.KeyAllowServerSSH, func(v bool) { bv := v; config.ServerSSHAllowed = &bv })
applyBool(mdm.KeyRemoteJobsAllowed, func(v bool) { bv := v; config.RemoteJobsAllowed = &bv })
applyBool(mdm.KeyAllowServerVNC, func(v bool) { bv := v; config.ServerVNCAllowed = &bv })
applyBool(mdm.KeyDisableVNCApproval, func(v bool) { bv := v; config.DisableVNCApproval = &bv })
applyBool(mdm.KeyDisableClientRoutes, func(v bool) { config.DisableClientRoutes = v })
@@ -798,6 +849,52 @@ func (config *Config) applyMDMPolicy(policy *mdm.Policy) {
config.LazyConnection = state
logApplied(mdm.KeyLazyConnection, state)
}
}
// ValidateBundleUploadURL sanity-checks a debug-bundle upload URL. An empty
// value is accepted — the executor falls back to the default upload service. A
// non-empty value must be a well-formed https URL with a host; a malformed
// value or a plaintext scheme is rejected. It deliberately does not constrain
// which host may receive the bundle. This is the single source of truth for the
// rule, shared by the remote-job executor (client/internal) and the MDM policy
// override below so the two validation paths cannot drift.
func ValidateBundleUploadURL(raw string) error {
if raw == "" {
return nil
}
parsed, err := url.Parse(raw)
if err != nil {
return fmt.Errorf("parse upload URL: %w", err)
}
// Hostname(), not Host: an authority like ":443" is non-empty but has no
// host, and would fail the actual upload.
if parsed.Scheme != "https" || parsed.Hostname() == "" {
return fmt.Errorf("upload URL must be an https URL with a host")
}
return nil
}
// mdmDebugBundleUploadURL resolves the MDM-enforced debug-bundle upload URL
// override from the policy, returning the empty string when the policy does
// not carry a valid KeyBundleUploadURL. An absent or invalid value fails
// closed to "" so it falls back to the management-supplied or default upload
// target rather than a previously-enforced one. The URL is never logged: it
// can embed credentials or signed query tokens (KeyBundleUploadURL is in
// mdm.SecretKeys).
func mdmDebugBundleUploadURL(policy *mdm.Policy) string {
v, ok := policy.GetString(mdm.KeyBundleUploadURL)
if !ok || v == "" {
return ""
}
// Must be a well-formed https URL with a host, matching the client's
// remote-job upload-URL validation (shared validator, single source of truth).
if err := ValidateBundleUploadURL(v); err != nil {
log.Warnf("MDM debug bundle upload URL is invalid (must be an https URL with a host); ignoring the override")
return ""
}
log.Infof("MDM override %s = ********** (secret)", mdm.KeyBundleUploadURL)
return v
}
// parseURL parses and validates the URL for the named service. The URL
@@ -14,6 +14,7 @@ import (
"github.com/netbirdio/netbird/client/iface"
"github.com/netbirdio/netbird/client/internal/routemanager/dynamic"
"github.com/netbirdio/netbird/client/mdm"
"github.com/netbirdio/netbird/util"
)
@@ -271,6 +272,83 @@ func TestUpdateConfigServerSSHAllowedNotSet(t *testing.T) {
}
}
func TestUpdateConfigRemoteJobsAllowed(t *testing.T) {
// Unlike SSH (which defaults on for legacy configs), remote jobs are an
// explicit opt-in: a pre-existing config with no value materializes to off.
t.Run("legacy config defaults off", func(t *testing.T) {
configPath := filepath.Join(t.TempDir(), "config.json")
require.NoError(t, os.WriteFile(configPath, []byte("{}"), 0600))
config, err := UpdateConfig(ConfigInput{ConfigPath: configPath})
require.NoError(t, err)
require.NotNil(t, config.RemoteJobsAllowed, "RemoteJobsAllowed should be materialized")
assert.False(t, *config.RemoteJobsAllowed, "remote jobs must default off")
})
for _, tt := range []struct {
name string
input *bool
want bool
}{
{"enable", util.True(), true},
{"disable", util.False(), false},
} {
t.Run(tt.name, func(t *testing.T) {
configPath := filepath.Join(t.TempDir(), "config.json")
require.NoError(t, os.WriteFile(configPath, []byte("{}"), 0600))
config, err := UpdateConfig(ConfigInput{ConfigPath: configPath, RemoteJobsAllowed: tt.input})
require.NoError(t, err)
require.NotNil(t, config.RemoteJobsAllowed)
assert.Equal(t, tt.want, *config.RemoteJobsAllowed)
})
}
}
func TestApplyMDMPolicyRemoteJobs(t *testing.T) {
t.Run("enables remote jobs and sets the upload URL override", func(t *testing.T) {
cfg := &Config{}
cfg.applyMDMPolicy(mdm.NewPolicy(map[string]any{
mdm.KeyRemoteJobsAllowed: true,
mdm.KeyBundleUploadURL: "https://upload.example.com",
}))
require.NotNil(t, cfg.RemoteJobsAllowed)
assert.True(t, *cfg.RemoteJobsAllowed, "MDM allowRemoteJobs must enable the flag")
assert.Equal(t, "https://upload.example.com", cfg.DebugBundleUploadURL, "MDM upload URL override must be applied")
})
t.Run("a non-https upload URL is rejected", func(t *testing.T) {
cfg := &Config{}
cfg.applyMDMPolicy(mdm.NewPolicy(map[string]any{
mdm.KeyBundleUploadURL: "http://insecure.example.com",
}))
assert.Empty(t, cfg.DebugBundleUploadURL, "a non-https upload URL must be skipped")
})
t.Run("dropping the key clears a previously-applied override", func(t *testing.T) {
cfg := &Config{DebugBundleUploadURL: "https://old.example.com"}
// A replacement policy that no longer carries the key must not leave
// the old upload target directing bundles.
cfg.applyMDMPolicy(mdm.NewPolicy(map[string]any{mdm.KeyRemoteJobsAllowed: true}))
assert.Empty(t, cfg.DebugBundleUploadURL, "the stale upload URL override must be cleared")
})
t.Run("an empty replacement policy clears a previously-applied override", func(t *testing.T) {
cfg := &Config{DebugBundleUploadURL: "https://old.example.com"}
// A policy that becomes empty entirely hits the IsEmpty early return;
// the override must still be cleared rather than surviving on the
// reused Config instance.
cfg.applyMDMPolicy(mdm.NewPolicy(map[string]any{}))
assert.Empty(t, cfg.DebugBundleUploadURL, "the stale upload URL override must be cleared when the policy empties")
})
t.Run("an invalid upload URL clears a previously-applied override (fail closed)", func(t *testing.T) {
cfg := &Config{DebugBundleUploadURL: "https://old.example.com"}
cfg.applyMDMPolicy(mdm.NewPolicy(map[string]any{mdm.KeyBundleUploadURL: "not-a-url"}))
assert.Empty(t, cfg.DebugBundleUploadURL, "an invalid override must fail closed, not keep the stale target")
})
}
func TestUpdateOldManagementURL(t *testing.T) {
origProber := newMgmProber
newMgmProber = func(_ context.Context, _ string, _ wgtypes.Key, _ bool) (mgmProber, error) {
@@ -0,0 +1,100 @@
package profilemanager
import (
"fmt"
"os"
"os/user"
"path/filepath"
"runtime"
log "github.com/sirupsen/logrus"
)
const envSudoUser = "SUDO_USER"
var (
geteuid = os.Geteuid
lookupUser = user.Lookup
)
// InvokingUser returns the user a CLI invocation acts for. Under sudo that is
// the user who ran sudo, not root: privileged flags force commands through
// sudo, and resolving profiles as root would silently switch the daemon to
// root's (default) profile instead of the invoking user's. Privilege decisions
// are not made here — those stay on the kernel credentials of the daemon
// connection, which SUDO_USER (a plain environment variable) can never
// influence; a forged value only selects a profile root could select anyway.
func InvokingUser() (*user.User, error) {
if u, ok := sudoInvokingUser(); ok {
return u, nil
}
// Fail closed instead of falling through to root: every caller feeds this
// username into profile-path resolution, so a lookup failure would resolve
// (and create) a root-owned profile namespace and switch the daemon onto it
// behind the invoking user's back.
if sudoActive() {
return nil, fmt.Errorf("resolve sudo invoking user %q: refusing to fall back to root", os.Getenv(envSudoUser))
}
return user.Current()
}
// IsPlainRoot reports that the process runs as root with no usable sudo
// context: there is no invoking user to act for, so per-user resolution falls
// back to root's own (empty) state. Callers use it to refuse ambiguous
// operations instead of silently acting on the wrong profile.
func IsPlainRoot() bool {
if geteuid() != 0 {
return false
}
_, ok := sudoInvokingUser()
return !ok
}
// MirrorIsAuthoritative reports whether the invoking user's local
// active-profile mirror can be trusted as the profile selector. It cannot under
// sudo (writes to it are skipped, so it goes stale) or as plain root (there is
// no invoking user, so it falls back to root's own default). Callers use it to
// decide whether to read the profile from the mirror or from the daemon.
func MirrorIsAuthoritative() bool {
return !sudoActive() && !IsPlainRoot()
}
// sudoInvokingUser resolves SUDO_USER when the process runs as root under
// sudo. Returns false whenever the sudo context is absent or unusable, in
// which case callers fall back to the process user.
func sudoInvokingUser() (*user.User, bool) {
if !sudoActive() {
return nil, false
}
name := os.Getenv(envSudoUser)
u, err := lookupUser(name)
if err != nil {
log.Warnf("sudo invoking user %q lookup: %v", name, err)
return nil, false
}
return u, true
}
// sudoActive reports a sudo context from the environment alone: write-skip
// decisions key off it so a transient user lookup failure can never flip a
// run from read-only to writing root-owned files into the user's directory.
func sudoActive() bool {
if geteuid() != 0 {
return false
}
name := os.Getenv(envSudoUser)
return name != "" && name != "root"
}
// userBaseConfigDir mirrors os.UserConfigDir for a user other than the process
// owner. Environment overrides (XDG_CONFIG_HOME) cannot be honoured here: under
// sudo the environment is root's, not the invoking user's.
func userBaseConfigDir(u *user.User) (string, error) {
if u.HomeDir == "" {
return "", fmt.Errorf("user %s has no home directory", u.Username)
}
if runtime.GOOS == "darwin" {
return filepath.Join(u.HomeDir, "Library", "Application Support"), nil
}
return filepath.Join(u.HomeDir, ".config"), nil
}
@@ -0,0 +1,230 @@
package profilemanager
import (
"errors"
"io/fs"
"os"
"os/user"
"path/filepath"
"runtime"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestInvokingUserFallsBackToProcessUser(t *testing.T) {
t.Setenv(envSudoUser, "")
got, err := InvokingUser()
require.NoError(t, err)
current, err := user.Current()
require.NoError(t, err)
assert.Equal(t, current.Username, got.Username)
}
func TestSudoInvokingUserInactiveWithoutSudoContext(t *testing.T) {
t.Setenv(envSudoUser, "")
_, ok := sudoInvokingUser()
assert.False(t, ok)
}
func TestSudoInvokingUserIgnoresRoot(t *testing.T) {
t.Setenv(envSudoUser, "root")
origEuid := geteuid
geteuid = func() int { return 0 }
t.Cleanup(func() { geteuid = origEuid })
_, ok := sudoInvokingUser()
assert.False(t, ok, "sudo from a root shell must not redirect anything")
assert.False(t, sudoActive())
assert.True(t, IsPlainRoot())
}
func TestSudoInvokingUserResolvesInvokingUser(t *testing.T) {
fakeSudo(t, filepath.Join("/home", "misha"))
u, ok := sudoInvokingUser()
require.True(t, ok)
assert.Equal(t, "misha", u.Username)
got, err := InvokingUser()
require.NoError(t, err)
assert.Equal(t, "misha", got.Username)
assert.False(t, IsPlainRoot())
}
func TestInvokingUserFailsClosedWhenSudoLookupFails(t *testing.T) {
fakeSudo(t, filepath.Join("/home", "misha"))
lookupUser = func(string) (*user.User, error) { return nil, errors.New("nss unavailable") }
got, err := InvokingUser()
require.Error(t, err)
assert.Nil(t, got, "must not resolve to the root process user")
}
func TestProfileFilePathFailsClosedWhenSudoLookupFails(t *testing.T) {
profilesRoot := t.TempDir()
fakeSudo(t, filepath.Join("/home", "misha"))
lookupUser = func(string) (*user.User, error) { return nil, errors.New("nss unavailable") }
origDir := DefaultConfigPathDir
DefaultConfigPathDir = profilesRoot
t.Cleanup(func() { DefaultConfigPathDir = origDir })
p := &Profile{ID: "0123456789abcdef0123456789abcdef"}
_, err := p.FilePath()
require.Error(t, err)
assertNoEntries(t, profilesRoot)
}
func TestSudoActiveSurvivesLookupFailure(t *testing.T) {
fakeSudo(t, filepath.Join("/home", "misha"))
lookupUser = func(string) (*user.User, error) { return nil, errors.New("nss unavailable") }
_, ok := sudoInvokingUser()
assert.False(t, ok)
assert.True(t, sudoActive())
assert.True(t, IsPlainRoot())
}
func TestGetConfigDirUnderSudoIsReadOnly(t *testing.T) {
home := t.TempDir()
fakeSudo(t, home)
base, err := baseConfigDir()
require.NoError(t, err)
if runtime.GOOS == "darwin" {
assert.Equal(t, filepath.Join(home, "Library", "Application Support"), base)
} else {
assert.Equal(t, filepath.Join(home, ".config"), base)
}
dir, err := getConfigDir()
require.NoError(t, err)
assert.Equal(t, filepath.Join(base, "netbird"), dir)
assert.NoDirExists(t, dir)
}
func TestBaseConfigDirFailsClosedWhenSudoLookupFails(t *testing.T) {
fakeSudo(t, filepath.Join("/home", "misha"))
lookupUser = func(string) (*user.User, error) { return nil, errors.New("nss unavailable") }
_, err := baseConfigDir()
require.Error(t, err)
_, err = getConfigDir()
require.Error(t, err)
}
func TestSwitchProfileSkipsStateWriteUnderSudo(t *testing.T) {
home := t.TempDir()
fakeSudo(t, home)
pm := NewProfileManager()
require.NoError(t, pm.SwitchProfile(defaultProfileName))
assertNoEntries(t, home)
}
func TestSetProfileStateSkipsWriteUnderSudo(t *testing.T) {
home := t.TempDir()
fakeSudo(t, home)
pm := NewProfileManager()
require.NoError(t, pm.SetProfileState(defaultProfileName, &ProfileState{Email: "misha@example.com"}))
assertNoEntries(t, home)
}
func TestRemoveProfileStateSkipsRemoveUnderSudo(t *testing.T) {
home := t.TempDir()
stateDir := filepath.Join(home, ".config", "netbird")
if runtime.GOOS == "darwin" {
stateDir = filepath.Join(home, "Library", "Application Support", "netbird")
}
require.NoError(t, os.MkdirAll(stateDir, 0o700))
stateFile := filepath.Join(stateDir, "default.state.json")
require.NoError(t, os.WriteFile(stateFile, []byte(`{"email":"misha@example.com"}`), 0o600))
fakeSudo(t, home)
pm := NewProfileManager()
require.NoError(t, pm.RemoveProfileState("default"))
assert.FileExists(t, stateFile)
}
func TestUserBaseConfigDir(t *testing.T) {
u := &user.User{Username: "misha", HomeDir: filepath.Join("/home", "misha")}
dir, err := userBaseConfigDir(u)
require.NoError(t, err)
if runtime.GOOS == "darwin" {
assert.Equal(t, filepath.Join(u.HomeDir, "Library", "Application Support"), dir)
} else {
assert.Equal(t, filepath.Join(u.HomeDir, ".config"), dir)
}
_, err = userBaseConfigDir(&user.User{Username: "nohome"})
require.Error(t, err)
}
func TestIsPlainRoot(t *testing.T) {
t.Setenv(envSudoUser, "")
origEuid := geteuid
t.Cleanup(func() { geteuid = origEuid })
geteuid = func() int { return 1000 }
assert.False(t, IsPlainRoot())
geteuid = func() int { return 0 }
assert.True(t, IsPlainRoot())
}
func TestMirrorIsAuthoritative(t *testing.T) {
t.Setenv(envSudoUser, "")
origEuid := geteuid
t.Cleanup(func() { geteuid = origEuid })
geteuid = func() int { return 1000 }
assert.True(t, MirrorIsAuthoritative(), "a normal user's own mirror is authoritative")
geteuid = func() int { return 0 }
assert.False(t, MirrorIsAuthoritative(), "plain root has no authoritative mirror")
}
func TestMirrorIsAuthoritativeFalseUnderSudo(t *testing.T) {
fakeSudo(t, filepath.Join("/home", "misha"))
assert.False(t, MirrorIsAuthoritative(), "the sudo mirror is frozen, so it is not authoritative")
}
func fakeSudo(t *testing.T, home string) {
t.Helper()
t.Setenv(envSudoUser, "misha")
origEuid := geteuid
origLookup := lookupUser
origOverride := ConfigDirOverride
geteuid = func() int { return 0 }
lookupUser = func(name string) (*user.User, error) {
return &user.User{Username: name, Uid: "1234", Gid: "1234", HomeDir: home}, nil
}
ConfigDirOverride = ""
t.Cleanup(func() {
geteuid = origEuid
lookupUser = origLookup
ConfigDirOverride = origOverride
})
}
func assertNoEntries(t *testing.T, root string) {
t.Helper()
err := filepath.WalkDir(root, func(path string, _ fs.DirEntry, err error) error {
if err != nil {
return err
}
if path != root {
t.Errorf("unexpected entry created under %s: %s", root, path)
}
return nil
})
require.NoError(t, err)
}
@@ -3,7 +3,6 @@ package profilemanager
import (
"fmt"
"os"
"os/user"
"path/filepath"
"strings"
"sync"
@@ -54,7 +53,7 @@ func (p *Profile) FilePath() (string, error) {
return "", fmt.Errorf("invalid profile ID: %q", id)
}
username, err := user.Current()
username, err := InvokingUser()
if err != nil {
return "", fmt.Errorf("failed to get current user: %w", err)
}
@@ -130,7 +129,7 @@ func (pm *ProfileManager) getActiveProfileState() ID {
if err != nil {
if !os.IsNotExist(err) {
log.Warnf("failed to read active profile state: %v", err)
} else {
} else if !sudoActive() {
if err := pm.setActiveProfileState(defaultProfileName); err != nil {
log.Warnf("failed to set default profile state: %v", err)
}
@@ -148,6 +147,13 @@ func (pm *ProfileManager) getActiveProfileState() ID {
}
func (pm *ProfileManager) setActiveProfileState(id ID) error {
// The invoking user's state is read-only under sudo — a root-owned file in
// the user's directory would break their own runs. The daemon still records
// the switch on its side; only the user-local bookkeeping is skipped.
if sudoActive() {
log.Infof("running under sudo: not persisting active profile %q for user %s", id, os.Getenv(envSudoUser))
return nil
}
configDir, err := getConfigDir()
if err != nil {
+16
View File
@@ -7,6 +7,8 @@ import (
"os"
"path/filepath"
log "github.com/sirupsen/logrus"
"github.com/netbirdio/netbird/util"
)
@@ -63,6 +65,15 @@ func (pm *ProfileManager) SetProfileState(id ID, state *ProfileState) error {
return fmt.Errorf("invalid profile ID: %q", id)
}
// The invoking user's state is read-only under sudo. The file only carries
// the account email for the login hint and display, so skipping the write
// costs at most one extra account prompt later — a root-owned file in the
// user's directory would cost every later update instead.
if sudoActive() {
log.Debugf("running under sudo: not persisting profile state for user %s", os.Getenv(envSudoUser))
return nil
}
stateFile := filepath.Join(configDir, id.String()+".state.json")
if err := util.WriteJsonWithRestrictedPermission(context.Background(), stateFile, state); err != nil {
return fmt.Errorf("write profile state: %w", err)
@@ -92,6 +103,11 @@ func (pm *ProfileManager) SetActiveProfileState(state *ProfileState) error {
// equivalent to clearing it; the next SSO login recreates it. A missing file
// is not an error.
func (pm *ProfileManager) RemoveProfileState(profileName string) error {
if sudoActive() {
log.Debugf("running under sudo: not removing profile state for user %s", os.Getenv(envSudoUser))
return nil
}
configDir, err := getConfigDir()
if err != nil {
return fmt.Errorf("get config directory: %w", err)