Add profile stamping as active migration

This commit is contained in:
Theodor S. Midtlien
2026-09-16 16:00:25 +02:00
committed by GitHub
parent 1ac303c77e
commit fe97bd5b4e
3 changed files with 392 additions and 20 deletions
+247 -4
View File
@@ -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()