mirror of
https://github.com/netbirdio/netbird.git
synced 2026-09-12 17:59:06 +02:00
Add profile stamping as active migration
This commit is contained in:
@@ -32,6 +32,10 @@ type Profile struct {
|
||||
Path string
|
||||
IsActive bool
|
||||
Owners []ipcauth.Principal
|
||||
// LegacyUserDir is the sanitized username of the per-username directory the
|
||||
// profile was found in, empty for the profiles.v1 directory and for the
|
||||
// default profile. It is the only legacy evidence of profile ownership.
|
||||
LegacyUserDir string
|
||||
}
|
||||
|
||||
// AccessibleBy reports whether a kernel-attested caller may address this
|
||||
@@ -43,9 +47,11 @@ func (p *Profile) AccessibleBy(id ipcauth.Identity) bool {
|
||||
if ipcauth.IsPrivilegedCaller(id) {
|
||||
return true
|
||||
}
|
||||
// TODO: decide on unowned behavior
|
||||
if len(p.Owners) == 0 {
|
||||
return false
|
||||
// A profile in a per-username directory belonged to a use, unowned fails
|
||||
// closed. The account named by the directory reclaims it on their next
|
||||
// lookup or until claimed by a privileged caller.
|
||||
return p.LegacyUserDir == ""
|
||||
}
|
||||
return p.Owners[0].Matches(id)
|
||||
}
|
||||
|
||||
@@ -10,11 +10,14 @@ import (
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/netbirdio/netbird/client/internal/getent"
|
||||
"github.com/netbirdio/netbird/client/internal/ipcauth"
|
||||
"github.com/netbirdio/netbird/util"
|
||||
)
|
||||
@@ -56,11 +59,11 @@ type profileMeta struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
// nolint:unused
|
||||
type ownerMeta struct {
|
||||
Owners []string
|
||||
}
|
||||
|
||||
// ownersFieldName is the key on disk.
|
||||
func (e *ErrAmbiguousHandle) Error() string {
|
||||
switch e.Kind {
|
||||
case AmbiguityKindIDPrefix:
|
||||
@@ -599,6 +602,8 @@ func (s *ServiceManager) loadAllProfilesForIdentity(userID ipcauth.Identity) ([]
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.claimLegacyProfiles(allProfiles, userID)
|
||||
|
||||
accessible := make([]Profile, 0, len(allProfiles))
|
||||
for _, p := range allProfiles {
|
||||
if p.AccessibleBy(userID) {
|
||||
@@ -609,6 +614,106 @@ func (s *ServiceManager) loadAllProfilesForIdentity(userID ipcauth.Identity) ([]
|
||||
return accessible, nil
|
||||
}
|
||||
|
||||
var (
|
||||
legacyDirMu sync.Mutex
|
||||
legacyDirCache = map[string]string{}
|
||||
)
|
||||
|
||||
// claimLegacyProfiles stamps the caller on every unowned profile in the
|
||||
// directory their own user name produced before the ownership model.
|
||||
//
|
||||
// Ownership lives in the file now, so the directory name is only a leftover.
|
||||
// Flattening is a separate step we are doing in the future. Moving it would
|
||||
// pull the state file out from under an engine that captured its path at
|
||||
// connect time.
|
||||
func (s *ServiceManager) claimLegacyProfiles(profiles []Profile, id ipcauth.Identity) {
|
||||
// A privileged caller reaches every profile already and an internal load
|
||||
// has no caller, so neither should leave an owner behind.
|
||||
if !id.Known() || ipcauth.IsPrivilegedCaller(id) {
|
||||
return
|
||||
}
|
||||
|
||||
if !hasUnownedLegacyProfile(profiles) {
|
||||
return
|
||||
}
|
||||
|
||||
dir, ok := legacyDirForIdentity(id)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
principal := ipcauth.OwnerPrincipalForIdentity(id)
|
||||
parsed, ok := ipcauth.ParsePrincipal(principal)
|
||||
if !ok {
|
||||
log.Warnf("not claiming legacy profiles, %q is not a usable owner", principal)
|
||||
return
|
||||
}
|
||||
|
||||
for i := range profiles {
|
||||
p := &profiles[i]
|
||||
if len(p.Owners) > 0 || p.LegacyUserDir == "" || p.LegacyUserDir != dir {
|
||||
continue
|
||||
}
|
||||
|
||||
if err := StampOwner(p.Path, id); err != nil {
|
||||
log.Warnf("could not claim legacy profile %s for %s: %v", p.Path, principal, err)
|
||||
continue
|
||||
}
|
||||
|
||||
p.Owners = []ipcauth.Principal{parsed}
|
||||
log.Infof("claimed legacy profile %s for %s, its directory is named after that account", p.Path, principal)
|
||||
}
|
||||
}
|
||||
|
||||
func hasUnownedLegacyProfile(profiles []Profile) bool {
|
||||
for i := range profiles {
|
||||
if profiles[i].LegacyUserDir != "" && len(profiles[i].Owners) == 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// legacyDirForIdentity is a variable so a test can supply an account name
|
||||
// without depending on the host's user database.
|
||||
var legacyDirForIdentity = resolveLegacyDir
|
||||
|
||||
// resolveLegacyDir returns the per-username directory the old layout would have
|
||||
// created for a caller, and whether there is one.
|
||||
//
|
||||
// Successes are cached for the process, failures are not, so a directory
|
||||
// service that is briefly unreachable does not lock its users out until the
|
||||
// daemon restarts.
|
||||
func resolveLegacyDir(id ipcauth.Identity) (string, bool) {
|
||||
key := ipcauth.OwnerPrincipalForIdentity(id)
|
||||
|
||||
legacyDirMu.Lock()
|
||||
cached, hit := legacyDirCache[key]
|
||||
legacyDirMu.Unlock()
|
||||
if hit {
|
||||
return cached, cached != ""
|
||||
}
|
||||
|
||||
lookup := strconv.FormatUint(uint64(id.UID), 10)
|
||||
if id.IsWindows() {
|
||||
lookup = id.SID
|
||||
}
|
||||
|
||||
u, err := getent.LookupUserID(lookup)
|
||||
if err != nil {
|
||||
log.Warnf("cannot resolve %s to an account name, its legacy profiles stay unowned: %v", key, err)
|
||||
return "", false
|
||||
}
|
||||
|
||||
dir := sanitizeProfileName(u.Username)
|
||||
|
||||
legacyDirMu.Lock()
|
||||
legacyDirCache[key] = dir
|
||||
legacyDirMu.Unlock()
|
||||
|
||||
return dir, dir != ""
|
||||
}
|
||||
|
||||
func (s *ServiceManager) loadAllProfiles() ([]Profile, error) {
|
||||
_, activeIsDefault := s.activeProfileID()
|
||||
defaultName := readProfileName(DefaultConfigPath)
|
||||
@@ -643,16 +748,23 @@ func (s *ServiceManager) loadAllProfiles() ([]Profile, error) {
|
||||
// from before the ID-keyed layout. The first is not necessarily under
|
||||
// DefaultConfigPathDir: a ServiceManager can be pointed at a directory of
|
||||
// its own, which is what the mobile bindings do.
|
||||
dirs := []string{s.profilesDirPath()}
|
||||
dirs := []profileDir{{path: s.profilesDirPath()}}
|
||||
|
||||
configPathDir, err := os.ReadDir(DefaultConfigPathDir)
|
||||
if err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||
return nil, fmt.Errorf("read profile directory: %w", err)
|
||||
}
|
||||
for _, entry := range configPathDir {
|
||||
if entry.IsDir() {
|
||||
dirs = append(dirs, filepath.Join(DefaultConfigPathDir, entry.Name()))
|
||||
if !entry.IsDir() || entry.Name() == DefaultProfilePathDir {
|
||||
continue
|
||||
}
|
||||
// Every other subdirectory is named after the account that created the
|
||||
// profiles in it. The dot in profiles.v1 is what keeps them apart,
|
||||
// since sanitizeProfileName drops dots.
|
||||
dirs = append(dirs, profileDir{
|
||||
path: filepath.Join(DefaultConfigPathDir, entry.Name()),
|
||||
legacyUser: entry.Name(),
|
||||
})
|
||||
}
|
||||
|
||||
var fileProfiles []Profile
|
||||
@@ -660,10 +772,10 @@ func (s *ServiceManager) loadAllProfiles() ([]Profile, error) {
|
||||
for _, dir := range dirs {
|
||||
// The profiles directory is usually one of the subdirectories above,
|
||||
// so without this a profile would be listed twice.
|
||||
if scanned[dir] {
|
||||
if scanned[dir.path] {
|
||||
continue
|
||||
}
|
||||
scanned[dir] = true
|
||||
scanned[dir.path] = true
|
||||
|
||||
dirProfiles, err := s.getProfilesFromDirectory(dir)
|
||||
if err != nil {
|
||||
@@ -683,7 +795,15 @@ func (s *ServiceManager) loadAllProfiles() ([]Profile, error) {
|
||||
return profiles, nil
|
||||
}
|
||||
|
||||
func (s *ServiceManager) getProfilesFromDirectory(configDir string) ([]Profile, error) {
|
||||
// profileDir is one directory the loader scans. legacyUser is the sanitized
|
||||
// username it is named after, empty for the directory profiles go to now.
|
||||
type profileDir struct {
|
||||
path string
|
||||
legacyUser string
|
||||
}
|
||||
|
||||
func (s *ServiceManager) getProfilesFromDirectory(dir profileDir) ([]Profile, error) {
|
||||
configDir := dir.path
|
||||
activeID, _ := s.activeProfileID()
|
||||
entries, err := os.ReadDir(configDir)
|
||||
if err != nil {
|
||||
@@ -725,11 +845,12 @@ func (s *ServiceManager) getProfilesFromDirectory(configDir string) ([]Profile,
|
||||
continue
|
||||
}
|
||||
fileProfiles = append(fileProfiles, Profile{
|
||||
ID: stem,
|
||||
Name: name,
|
||||
Path: path,
|
||||
IsActive: stem == ID(activeID),
|
||||
Owners: owners,
|
||||
ID: stem,
|
||||
Name: name,
|
||||
Path: path,
|
||||
IsActive: stem == ID(activeID),
|
||||
Owners: owners,
|
||||
LegacyUserDir: dir.legacyUser,
|
||||
})
|
||||
}
|
||||
return fileProfiles, nil
|
||||
@@ -777,12 +898,14 @@ func readProfileOwners(path string) ([]ipcauth.Principal, error) {
|
||||
return []ipcauth.Principal{principal}, nil
|
||||
}
|
||||
|
||||
// nolint: unused,unusedfunc
|
||||
// StampOwner records owner as a profile's owner, replacing whoever is recorded
|
||||
// now.
|
||||
func StampOwner(path string, owner ipcauth.Identity) error {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var cfg Config
|
||||
if err := json.Unmarshal(data, &cfg); err != nil {
|
||||
return err
|
||||
@@ -790,7 +913,7 @@ func StampOwner(path string, owner ipcauth.Identity) error {
|
||||
cfg.Owners = []string{ipcauth.OwnerPrincipalForIdentity(owner)}
|
||||
|
||||
if err := util.WriteJson(context.Background(), path, cfg); err != nil {
|
||||
return fmt.Errorf("failed to write profile owner: %w", err)
|
||||
return fmt.Errorf("write profile owner: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -2,10 +2,12 @@ package profilemanager
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"os"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
@@ -289,7 +291,7 @@ func TestListProfiles_PrivilegedResolvesUnfiltered(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestListProfiles_UnownedIsPrivilegedOnly(t *testing.T) {
|
||||
func TestListProfiles_UnownedOutsideALegacyDirIsOpen(t *testing.T) {
|
||||
withTestSM(t, func(sm *ServiceManager, _ ipcauth.Identity) {
|
||||
unowned, err := sm.AddProfile("unowned", nil)
|
||||
require.NoError(t, err)
|
||||
@@ -297,16 +299,257 @@ func TestListProfiles_UnownedIsPrivilegedOnly(t *testing.T) {
|
||||
alice := ipcauth.KnownForTest(ipcauth.Identity{UID: 4242})
|
||||
got, err := sm.ListProfiles(alice)
|
||||
require.NoError(t, err)
|
||||
assert.NotContains(t, profileIDs(got), unowned.ID.String(),
|
||||
"an unowned profile is not addressable until it is claimed")
|
||||
assert.Contains(t, profileIDs(got), unowned.ID.String(),
|
||||
"a profile that never had an owner stays usable until someone claims it")
|
||||
assert.Contains(t, profileIDs(got), defaultProfileName,
|
||||
"a fresh install has to be usable before anything is claimed")
|
||||
|
||||
nobody, err := sm.ListProfiles(ipcauth.Identity{})
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, profileIDs(nobody), "an unattested caller reaches nothing")
|
||||
})
|
||||
}
|
||||
|
||||
// withLegacyLayout wires up the globals the way withTestSM does, without the
|
||||
// identity, since these tests supply their own.
|
||||
func withLegacyLayout(t *testing.T, fn func(sm *ServiceManager, configDir string)) {
|
||||
t.Helper()
|
||||
withTempConfigDir(t, func(configDir string) {
|
||||
withPatchedGlobals(t, configDir, func() {
|
||||
sm := &ServiceManager{}
|
||||
require.NoError(t, sm.CreateDefaultProfile())
|
||||
fn(sm, configDir)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// writeLegacyProfile drops a profile into a per-username directory, the layout
|
||||
// every profile used before the ID-keyed one. It writes no name, since a legacy
|
||||
// profile took its display name from the filename.
|
||||
func writeLegacyProfile(t *testing.T, configDir, dirName, id string, fields map[string]any) string {
|
||||
t.Helper()
|
||||
dir := filepath.Join(configDir, dirName)
|
||||
require.NoError(t, os.MkdirAll(dir, 0700))
|
||||
|
||||
doc := map[string]any{}
|
||||
for k, v := range fields {
|
||||
doc[k] = v
|
||||
}
|
||||
path := filepath.Join(dir, id+".json")
|
||||
require.NoError(t, util.WriteJson(context.Background(), path, doc))
|
||||
return path
|
||||
}
|
||||
|
||||
// stubLegacyDir replaces the account lookup, so the claim path can be exercised
|
||||
// without an account existing on the machine running the test.
|
||||
func stubLegacyDir(t *testing.T, dir string) {
|
||||
t.Helper()
|
||||
orig := legacyDirForIdentity
|
||||
legacyDirForIdentity = func(ipcauth.Identity) (string, bool) { return dir, dir != "" }
|
||||
t.Cleanup(func() { legacyDirForIdentity = orig })
|
||||
}
|
||||
|
||||
func readOwners(t *testing.T, path string) []string {
|
||||
t.Helper()
|
||||
data, err := os.ReadFile(path)
|
||||
require.NoError(t, err)
|
||||
var meta ownerMeta
|
||||
require.NoError(t, json.Unmarshal(data, &meta))
|
||||
return meta.Owners
|
||||
}
|
||||
|
||||
func TestListProfiles_UnownedLegacyProfileIsPrivilegedOnly(t *testing.T) {
|
||||
withLegacyLayout(t, func(sm *ServiceManager, configDir string) {
|
||||
path := writeLegacyProfile(t, configDir, "alice", "work", nil)
|
||||
stubLegacyDir(t, "bob")
|
||||
|
||||
bob := ipcauth.KnownForTest(ipcauth.Identity{UID: 4242})
|
||||
got, err := sm.ListProfiles(bob)
|
||||
require.NoError(t, err)
|
||||
assert.NotContains(t, profileIDs(got), "work",
|
||||
"a profile sitting in someone else's directory is not free to take")
|
||||
assert.Empty(t, readOwners(t, path), "and it is not claimed on the way past")
|
||||
|
||||
root := ipcauth.KnownForTest(ipcauth.Identity{UID: 0})
|
||||
got, err = sm.ListProfiles(root)
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, profileIDs(got), unowned.ID.String())
|
||||
assert.Contains(t, profileIDs(got), "work",
|
||||
"root still reaches it, which is how it gets reassigned")
|
||||
})
|
||||
}
|
||||
|
||||
func TestListProfiles_ClaimsLegacyProfileForItsOwnAccount(t *testing.T) {
|
||||
withLegacyLayout(t, func(sm *ServiceManager, configDir string) {
|
||||
path := writeLegacyProfile(t, configDir, "alice", "work", nil)
|
||||
stubLegacyDir(t, "alice")
|
||||
|
||||
alice := ipcauth.KnownForTest(ipcauth.Identity{UID: 4242})
|
||||
got, err := sm.ListProfiles(alice)
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, profileIDs(got), "work",
|
||||
"the claim lands before the listing is filtered, so the gap closes in one call")
|
||||
assert.Equal(t, []string{"uid:4242"}, readOwners(t, path))
|
||||
|
||||
// The claim is on disk now, so it is the owner check and not the
|
||||
// directory name that keeps the next caller out.
|
||||
stubLegacyDir(t, "alice")
|
||||
other := ipcauth.KnownForTest(ipcauth.Identity{UID: 5252})
|
||||
got, err = sm.ListProfiles(other)
|
||||
require.NoError(t, err)
|
||||
assert.NotContains(t, profileIDs(got), "work")
|
||||
assert.Equal(t, []string{"uid:4242"}, readOwners(t, path),
|
||||
"a second caller does not overwrite a stamped owner")
|
||||
})
|
||||
}
|
||||
|
||||
func TestClaimLegacyProfile_LeavesTheProfileWhereItIs(t *testing.T) {
|
||||
withLegacyLayout(t, func(sm *ServiceManager, configDir string) {
|
||||
path := writeLegacyProfile(t, configDir, "alice", "work", nil)
|
||||
for _, suffix := range []string{".state.json", prefsFileSuffix} {
|
||||
require.NoError(t, os.WriteFile(filepath.Join(configDir, "alice", "work"+suffix), []byte("{}"), 0600))
|
||||
}
|
||||
stubLegacyDir(t, "alice")
|
||||
|
||||
alice := ipcauth.KnownForTest(ipcauth.Identity{UID: 4242})
|
||||
got, err := sm.ListProfiles(alice)
|
||||
require.NoError(t, err)
|
||||
|
||||
claimed := ownedProfile(t, got, "uid:4242")
|
||||
assert.Equal(t, ID("work"), claimed.ID, "claiming does not re-key the profile")
|
||||
assert.Equal(t, path, claimed.Path)
|
||||
assert.Equal(t, "alice", claimed.LegacyUserDir,
|
||||
"the directory is a leftover now, not something the claim rewrites")
|
||||
|
||||
// The engine captures its state file path at connect time and writes
|
||||
// back to whatever it captured, so nothing here may move that file.
|
||||
assert.FileExists(t, filepath.Join(configDir, "alice", "work.state.json"))
|
||||
assert.FileExists(t, filepath.Join(configDir, "alice", "work"+prefsFileSuffix))
|
||||
assert.NoFileExists(t, filepath.Join(configDir, DefaultProfilePathDir, "work.json"))
|
||||
})
|
||||
}
|
||||
|
||||
func TestClaimLegacyProfile_NamesakeInAnotherDirectoryIsUntouched(t *testing.T) {
|
||||
withLegacyLayout(t, func(sm *ServiceManager, configDir string) {
|
||||
writeLegacyProfile(t, configDir, "alice", "work", nil)
|
||||
bobs := writeLegacyProfile(t, configDir, "bob", "work", nil)
|
||||
stubLegacyDir(t, "alice")
|
||||
|
||||
alice := ipcauth.KnownForTest(ipcauth.Identity{UID: 4242})
|
||||
got, err := sm.ListProfiles(alice)
|
||||
require.NoError(t, err)
|
||||
|
||||
claimed := ownedProfile(t, got, "uid:4242")
|
||||
assert.Equal(t, filepath.Join(configDir, "alice", "work.json"), claimed.Path,
|
||||
"a legacy ID is a display name two accounts can hold, so the path is what tells them apart")
|
||||
assert.Empty(t, readOwners(t, bobs), "bob's namesake is not claimed")
|
||||
})
|
||||
}
|
||||
|
||||
func ownedProfile(t *testing.T, profiles []Profile, principal string) Profile {
|
||||
t.Helper()
|
||||
var found []Profile
|
||||
for _, p := range profiles {
|
||||
if len(p.Owners) == 1 && p.Owners[0].String() == principal {
|
||||
found = append(found, p)
|
||||
}
|
||||
}
|
||||
require.Len(t, found, 1, "exactly one profile is owned by %s", principal)
|
||||
return found[0]
|
||||
}
|
||||
|
||||
func TestClaimLegacyProfile_SkipsOneAlreadyOwnedInTheSameDirectory(t *testing.T) {
|
||||
withLegacyLayout(t, func(sm *ServiceManager, configDir string) {
|
||||
// One unowned profile keeps the claim running over the directory, so
|
||||
// the owned one beside it is reached and has to be left alone.
|
||||
taken := writeLegacyProfile(t, configDir, "alice", "taken", map[string]any{
|
||||
"Owners": []string{"uid:9999"},
|
||||
})
|
||||
free := writeLegacyProfile(t, configDir, "alice", "free", nil)
|
||||
stubLegacyDir(t, "alice")
|
||||
|
||||
alice := ipcauth.KnownForTest(ipcauth.Identity{UID: 4242})
|
||||
_, err := sm.ListProfiles(alice)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, []string{"uid:9999"}, readOwners(t, taken),
|
||||
"a profile that already has an owner is not restamped")
|
||||
assert.Equal(t, []string{"uid:4242"}, readOwners(t, free))
|
||||
})
|
||||
}
|
||||
|
||||
func TestListProfiles_PrivilegedCallerDoesNotClaim(t *testing.T) {
|
||||
withLegacyLayout(t, func(sm *ServiceManager, configDir string) {
|
||||
path := writeLegacyProfile(t, configDir, "root", "work", nil)
|
||||
stubLegacyDir(t, "root")
|
||||
|
||||
root := ipcauth.KnownForTest(ipcauth.Identity{UID: 0})
|
||||
got, err := sm.ListProfiles(root)
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, profileIDs(got), "work")
|
||||
assert.Empty(t, readOwners(t, path),
|
||||
"root reaches every profile anyway, so a listing must not stamp one")
|
||||
})
|
||||
}
|
||||
|
||||
func TestStampOwner(t *testing.T) {
|
||||
withLegacyLayout(t, func(_ *ServiceManager, configDir string) {
|
||||
path := writeLegacyProfile(t, configDir, "alice", "work", map[string]any{
|
||||
"DisableAutoConnect": true,
|
||||
})
|
||||
|
||||
require.NoError(t, StampOwner(path, ipcauth.KnownForTest(ipcauth.Identity{UID: 4242})))
|
||||
assert.Equal(t, []string{"uid:4242"}, readOwners(t, path))
|
||||
|
||||
var fields map[string]any
|
||||
data, err := os.ReadFile(path)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, json.Unmarshal(data, &fields))
|
||||
assert.Equal(t, true, fields["DisableAutoConnect"],
|
||||
"the rest of the config survives the stamp")
|
||||
|
||||
require.NoError(t, StampOwner(path, ipcauth.KnownForTest(ipcauth.Identity{UID: 5252})))
|
||||
assert.Equal(t, []string{"uid:5252"}, readOwners(t, path),
|
||||
"stamping replaces whoever is recorded, the caller decides whether to")
|
||||
})
|
||||
}
|
||||
|
||||
func TestClaimLegacyProfile_LeavesAnOwnerItCannotParse(t *testing.T) {
|
||||
withLegacyLayout(t, func(sm *ServiceManager, configDir string) {
|
||||
// A principal kind this build does not know, which is what a client
|
||||
// newer than this one leaves behind after a downgrade. The loader
|
||||
// drops such a profile rather than reporting it as unowned, which is
|
||||
// what keeps the claim from treating it as free to take.
|
||||
path := writeLegacyProfile(t, configDir, "alice", "work", map[string]any{
|
||||
"Owners": []string{"group:devs"},
|
||||
})
|
||||
stubLegacyDir(t, "alice")
|
||||
|
||||
alice := ipcauth.KnownForTest(ipcauth.Identity{UID: 4242})
|
||||
got, err := sm.ListProfiles(alice)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.NotContains(t, profileIDs(got), "work")
|
||||
assert.Equal(t, []string{"group:devs"}, readOwners(t, path),
|
||||
"the profile is not taken over, it waits for an explicit claim")
|
||||
})
|
||||
}
|
||||
|
||||
func TestResolveLegacyDir_SanitizesTheSameStringTheOldLayoutDid(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("the windows path resolves a SID rather than a numeric uid")
|
||||
}
|
||||
|
||||
u, err := user.Current()
|
||||
require.NoError(t, err)
|
||||
uid, err := strconv.ParseUint(u.Uid, 10, 32)
|
||||
require.NoError(t, err)
|
||||
|
||||
got, ok := resolveLegacyDir(ipcauth.KnownForTest(ipcauth.Identity{UID: uint32(uid)}))
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, sanitizeProfileName(u.Username), got,
|
||||
"the directory has to come from the account name, sanitized the way the old layout did")
|
||||
}
|
||||
|
||||
func TestListProfiles_UnreadableOwnersAreSkipped(t *testing.T) {
|
||||
withTestSM(t, func(sm *ServiceManager, _ ipcauth.Identity) {
|
||||
configDir, err := sm.getConfigDir()
|
||||
|
||||
Reference in New Issue
Block a user