Move active migration to OwnsProfile

This commit is contained in:
Theodor S. Midtlien
2026-09-17 19:57:40 +02:00
parent cec9ee6699
commit 96f9ea7428
3 changed files with 65 additions and 108 deletions
+27 -17
View File
@@ -426,7 +426,7 @@ func (s *ServiceManager) AddProfile(displayName string, callerId *ipcauth.Identi
}, nil
}
func (s *ServiceManager) RenameProfile(id ID, userID ipcauth.Identity, newName string) error {
func (s *ServiceManager) RenameProfile(id ID, newName string) error {
displayName, err := sanitizeDisplayName(newName)
if err != nil {
return fmt.Errorf("invalid profile name: %w", err)
@@ -436,7 +436,7 @@ func (s *ServiceManager) RenameProfile(id ID, userID ipcauth.Identity, newName s
return fmt.Errorf("invalid profile ID: %q", id)
}
profiles, err := s.loadAllProfilesForIdentity(userID)
profiles, err := s.loadAllProfiles()
if err != nil {
return fmt.Errorf("load profiles: %w", err)
}
@@ -458,7 +458,7 @@ func (s *ServiceManager) RenameProfile(id ID, userID ipcauth.Identity, newName s
// RemoveProfile deletes the profile identified by id. Callers must have
// already resolved any user-supplied handle to a concrete ID via
// ResolveProfile.
func (s *ServiceManager) RemoveProfile(id ID, userID ipcauth.Identity) error {
func (s *ServiceManager) RemoveProfile(id ID) error {
if id == defaultProfileName {
defaultName := readProfileName(DefaultConfigPath)
if defaultName == "" {
@@ -470,7 +470,7 @@ func (s *ServiceManager) RemoveProfile(id ID, userID ipcauth.Identity) error {
return fmt.Errorf("invalid profile ID: %q", id)
}
profiles, err := s.loadAllProfilesForIdentity(userID)
profiles, err := s.loadAllProfiles()
if err != nil {
return fmt.Errorf("load profiles: %w", err)
}
@@ -511,8 +511,7 @@ func (s *ServiceManager) RemoveProfile(id ID, userID ipcauth.Identity) error {
return nil
}
// ListProfiles returns every profile for the given user, including the
// default profile, with IsActive flags set.
// ListProfiles returns every profile for the given user
func (s *ServiceManager) ListProfiles(userID ipcauth.Identity) ([]Profile, error) {
return s.loadAllProfilesForIdentity(userID)
}
@@ -588,22 +587,21 @@ func (s *ServiceManager) profilesDirPath() string {
return filepath.Join(DefaultConfigPathDir, DefaultProfilePathDir)
}
// loadAllProfiles returns every profile visible to the daemon for the
// given user, including the default profile. The returned slice is sorted
// by ID for a stable display order.
// loadAllProfiles returns every profile acessible by a given kernal attested
// user. The returned slice is sorted by ID for a stable display order.
//
// Each Profile is fully populated: ID is the filename stem, Name comes
// from the JSON's "name" field (falling back to the filename stem when absent)
// and Path is built from a basename read off disk.
func (s *ServiceManager) loadAllProfilesForIdentity(userID ipcauth.Identity) ([]Profile, error) {
if !userID.Known() {
return []Profile{}, nil
}
allProfiles, err := s.loadAllProfiles()
if err != nil {
return nil, err
}
s.claimDefaultProfileIfNeeded(allProfiles, userID)
s.claimLegacyProfiles(allProfiles, userID)
accessible := make([]Profile, 0, len(allProfiles))
for _, p := range allProfiles {
if p.AccessibleBy(userID) {
@@ -619,20 +617,26 @@ var (
legacyDirCache = map[string]string{}
)
// claimLegacyProfiles stamps the caller on every unowned profile in the
// 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) {
func (s *ServiceManager) ClaimLegacyProfiles(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 ipcauth.IsPrivilegedCaller(id) {
return
}
profiles, err := s.loadAllProfiles()
if err != nil {
log.Warnf("could not load all profiles: %v", err)
return
}
if !hasUnownedLegacyProfile(profiles) {
return
}
@@ -665,11 +669,17 @@ func (s *ServiceManager) claimLegacyProfiles(profiles []Profile, id ipcauth.Iden
}
}
func (s *ServiceManager) claimDefaultProfileIfNeeded(profiles []Profile, id ipcauth.Identity) {
func (s *ServiceManager) ClaimDefaultProfileIfNeeded(id ipcauth.Identity) {
if !id.Known() || ipcauth.IsPrivilegedCaller(id) {
return
}
profiles, err := s.loadAllProfiles()
if err != nil {
log.Warnf("could not load all profiles: %w", err)
return
}
var unowned bool
var p *Profile
for i := range profiles {
@@ -1055,12 +1065,12 @@ func (s *ServiceManager) activeProfileID() (ID, bool) {
// precedence is: exact ID match, then unique exact name, then unique ID
// prefix. Ambiguous matches return *ErrAmbiguousHandle so callers can
// surface the candidates.
func (s *ServiceManager) ResolveProfile(handle string, userID ipcauth.Identity) (*Profile, error) {
func (s *ServiceManager) ResolveProfile(handle string) (*Profile, error) {
if handle == "" {
return nil, fmt.Errorf("profile handle is empty")
}
profiles, err := s.loadAllProfilesForIdentity(userID)
profiles, err := s.loadAllProfiles()
if err != nil {
return nil, err
}
+3 -3
View File
@@ -132,7 +132,7 @@ func (pm *ProfileManager) GetActiveProfile() (*Profile, error) {
return nil, fmt.Errorf("get active profile: %w", err)
}
prof, err := pm.serviceMgr.ResolveProfile(activeState.ID.String(), pm.identity)
prof, err := pm.serviceMgr.ResolveProfile(activeState.ID.String())
if err != nil {
return nil, fmt.Errorf("resolve active profile %q: %w", activeState.ID, err)
}
@@ -182,7 +182,7 @@ func (pm *ProfileManager) RenameProfile(id string, newName string) error {
if err := pm.checkProfilesAllowed(); err != nil {
return err
}
if err := pm.serviceMgr.RenameProfile(profilemanager.ID(id), pm.identity, newName); err != nil {
if err := pm.serviceMgr.RenameProfile(profilemanager.ID(id), newName); err != nil {
return fmt.Errorf("rename profile: %w", err)
}
@@ -236,7 +236,7 @@ func (pm *ProfileManager) RemoveProfile(id string) error {
return err
}
if err := pm.serviceMgr.RemoveProfile(profilemanager.ID(id), pm.identity); err != nil {
if err := pm.serviceMgr.RemoveProfile(profilemanager.ID(id)); err != nil {
return fmt.Errorf("remove profile: %w", err)
}
+35 -88
View File
@@ -533,12 +533,7 @@ func (s *Server) SetConfig(callerCtx context.Context, msg *proto.SetConfigReques
return nil, err
}
callerID, err := callerIdentity(callerCtx)
if err != nil {
return nil, err
}
stored, err := s.storedProfileConfig(msg.ProfileName, callerID)
stored, err := s.storedProfileConfig(msg.ProfileName)
if err != nil {
return nil, err
}
@@ -546,7 +541,7 @@ func (s *Server) SetConfig(callerCtx context.Context, msg *proto.SetConfigReques
return nil, err
}
config, err := s.setConfigInputFromRequest(msg, callerID)
config, err := s.setConfigInputFromRequest(msg)
if err != nil {
return nil, err
}
@@ -576,10 +571,10 @@ func (s *Server) SetConfig(callerCtx context.Context, msg *proto.SetConfigReques
// field is its own optional case. Returns the resolved ConfigInput
// and a non-nil error only when the active profile file path cannot
// be determined.
func (s *Server) setConfigInputFromRequest(msg *proto.SetConfigRequest, callerID ipcauth.Identity) (profilemanager.ConfigInput, error) {
func (s *Server) setConfigInputFromRequest(msg *proto.SetConfigRequest) (profilemanager.ConfigInput, error) {
var config profilemanager.ConfigInput
resolved, err := s.resolveProfileHandle(msg.ProfileName, callerID)
resolved, err := s.resolveProfileHandle(msg.ProfileName)
if err != nil {
log.Errorf("failed to resolve profile %q: %v", msg.ProfileName, err)
return config, err
@@ -682,11 +677,6 @@ func (s *Server) Login(callerCtx context.Context, msg *proto.LoginRequest) (*pro
}
}
callerID, err := callerIdentity(callerCtx)
if err != nil {
return nil, err
}
activeProf, err := s.profileManager.GetActiveProfileState()
if err != nil {
log.Errorf("failed to get active profile state: %v", err)
@@ -698,7 +688,7 @@ func (s *Server) Login(callerCtx context.Context, msg *proto.LoginRequest) (*pro
// refused login neither switches the profile nor cancels a login already in
// progress, and it reads the profile the request targets, which is the one the
// switch below would activate.
stored, err := s.storedLoginConfig(activeProf, msg, callerID)
stored, err := s.storedLoginConfig(activeProf, msg)
if err != nil {
return nil, err
}
@@ -1092,12 +1082,6 @@ func (s *Server) Up(callerCtx context.Context, msg *proto.UpRequest) (*proto.UpR
return nil, fmt.Errorf("config is not defined, please call login command first")
}
callerID, err := callerIdentity(callerCtx)
if err != nil {
s.mutex.Unlock()
return nil, err
}
activeProf, err := s.profileManager.GetActiveProfileState()
if err != nil {
s.mutex.Unlock()
@@ -1106,7 +1090,7 @@ func (s *Server) Up(callerCtx context.Context, msg *proto.UpRequest) (*proto.UpR
}
if msg != nil && msg.ProfileName != nil {
if _, err := s.switchProfileIfNeeded(*msg.ProfileName, callerID, activeProf); err != nil {
if _, err := s.switchProfileIfNeeded(*msg.ProfileName, activeProf); err != nil {
s.mutex.Unlock()
log.Errorf("failed to switch profile: %v", err)
return nil, err
@@ -1172,8 +1156,8 @@ func (s *Server) waitForUp(callerCtx context.Context) (*proto.UpResponse, error)
// targets, so a privileged-change decision can be made against the values the
// profile currently holds. A profile that has no config file yet yields nil,
// which every caller must read as "nothing enabled yet".
func (s *Server) storedProfileConfig(handle string, callerID ipcauth.Identity) (*profilemanager.Config, error) {
resolved, err := s.resolveProfileHandle(handle, callerID)
func (s *Server) storedProfileConfig(handle string) (*profilemanager.Config, error) {
resolved, err := s.resolveProfileHandle(handle)
if err != nil {
return nil, err
}
@@ -1189,7 +1173,7 @@ func (s *Server) storedProfileConfig(handle string, callerID ipcauth.Identity) (
// storedLoginConfig loads the on-disk config of the profile a login request
// targets: the one it names, or the active one when it names none. Used to decide
// a privileged change before the request is allowed to switch profiles.
func (s *Server) storedLoginConfig(activeProf *profilemanager.ActiveProfileState, msg *proto.LoginRequest, callerID ipcauth.Identity) (*profilemanager.Config, error) {
func (s *Server) storedLoginConfig(activeProf *profilemanager.ActiveProfileState, msg *proto.LoginRequest) (*profilemanager.Config, error) {
if msg.ProfileName == nil {
cfgPath, err := s.profileManager.ActiveProfilePath(activeProf)
if err != nil {
@@ -1200,7 +1184,7 @@ func (s *Server) storedLoginConfig(activeProf *profilemanager.ActiveProfileState
// Mirrors switchProfileIfNeeded, so this reads the very profile the switch
// would activate.
return s.storedProfileConfig(*msg.ProfileName, callerID)
return s.storedProfileConfig(*msg.ProfileName)
}
// storedConfigAtPath reads a profile config file, yielding nil when it does not
@@ -1235,11 +1219,10 @@ func callerIdentity(ctx context.Context) (ipcauth.Identity, error) {
}
// resolveProfileHandle resolves a wire-level profile handle (display
// name, ID, or unique ID prefix) to a concrete profile owned by, or open to,
// the calling identity. Returns gRPC status errors so handlers can return them
// directly.
func (s *Server) resolveProfileHandle(handle string, callerID ipcauth.Identity) (*profilemanager.Profile, error) {
p, err := s.profileManager.ResolveProfile(handle, callerID)
// name, ID, or unique ID prefix). Returns gRPC status errors so
// handlers can return them directly.
func (s *Server) resolveProfileHandle(handle string) (*profilemanager.Profile, error) {
p, err := s.profileManager.ResolveProfile(handle)
if err == nil {
return p, nil
}
@@ -1256,8 +1239,8 @@ func (s *Server) resolveProfileHandle(handle string, callerID ipcauth.Identity)
// switchProfileIfNeeded resolves the user-supplied handle, updates the
// active profile state if it differs from the current one, and returns
// the resolved profile so callers can include its ID in RPC responses.
func (s *Server) switchProfileIfNeeded(handle string, callerID ipcauth.Identity, activeProf *profilemanager.ActiveProfileState) (*profilemanager.Profile, error) {
resolved, err := s.resolveProfileHandle(handle, callerID)
func (s *Server) switchProfileIfNeeded(handle string, activeProf *profilemanager.ActiveProfileState) (*profilemanager.Profile, error) {
resolved, err := s.resolveProfileHandle(handle)
if err != nil {
return nil, err
}
@@ -1271,7 +1254,7 @@ func (s *Server) switchProfileIfNeeded(handle string, callerID ipcauth.Identity,
return nil, gstatus.Errorf(codes.Unavailable, errProfilesDisabled)
}
log.Infof("switching to profile %s (%s) for %s", resolved.Name, resolved.ID, callerID)
log.Infof("switching to profile %s (%s)", resolved.Name, resolved.ID)
if err := s.profileManager.SetActiveProfileState(&profilemanager.ActiveProfileState{
ID: resolved.ID,
Username: legacyDirHint(resolved),
@@ -1288,11 +1271,6 @@ func (s *Server) SwitchProfile(callerCtx context.Context, msg *proto.SwitchProfi
s.mutex.Lock()
defer s.mutex.Unlock()
callerID, err := callerIdentity(callerCtx)
if err != nil {
return nil, err
}
activeProf, err := s.profileManager.GetActiveProfileState()
if err != nil {
log.Errorf("failed to get active profile state: %v", err)
@@ -1300,7 +1278,7 @@ func (s *Server) SwitchProfile(callerCtx context.Context, msg *proto.SwitchProfi
}
if msg != nil && msg.ProfileName != nil {
if _, err := s.switchProfileIfNeeded(*msg.ProfileName, callerID, activeProf); err != nil {
if _, err := s.switchProfileIfNeeded(*msg.ProfileName, activeProf); err != nil {
log.Errorf("failed to switch profile: %v", err)
return nil, err
}
@@ -1453,12 +1431,7 @@ func (s *Server) Logout(ctx context.Context, msg *proto.LogoutRequest) (*proto.L
}
func (s *Server) handleProfileLogout(ctx context.Context, msg *proto.LogoutRequest) (*proto.LogoutResponse, error) {
callerID, err := callerIdentity(ctx)
if err != nil {
return nil, err
}
resolved, err := s.resolveProfileHandle(*msg.ProfileName, callerID)
resolved, err := s.resolveProfileHandle(*msg.ProfileName)
if err != nil {
return nil, err
}
@@ -2270,12 +2243,7 @@ func (s *Server) GetConfig(ctx context.Context, req *proto.GetConfigRequest) (*p
return nil, ctx.Err()
}
callerID, err := callerIdentity(ctx)
if err != nil {
return nil, err
}
resolved, err := s.resolveProfileHandle(req.ProfileName, callerID)
resolved, err := s.resolveProfileHandle(req.ProfileName)
if err != nil {
log.Errorf("failed to resolve profile %q: %v", req.ProfileName, err)
return nil, err
@@ -2420,17 +2388,12 @@ func (s *Server) RenameProfile(ctx context.Context, msg *proto.RenameProfileRequ
return nil, gstatus.Errorf(codes.InvalidArgument, "profile name and new profile name must be provided")
}
callerID, err := callerIdentity(ctx)
resolved, err := s.resolveProfileHandle(msg.Handle)
if err != nil {
return nil, err
}
resolved, err := s.resolveProfileHandle(msg.Handle, callerID)
if err != nil {
return nil, err
}
err = s.profileManager.RenameProfile(resolved.ID, callerID, msg.NewProfileName)
err = s.profileManager.RenameProfile(resolved.ID, msg.NewProfileName)
if err != nil {
log.Errorf("failed to rename profile: %v", err)
return nil, fmt.Errorf("failed to rename profile: %w", err)
@@ -2454,12 +2417,7 @@ func (s *Server) RemoveProfile(ctx context.Context, msg *proto.RemoveProfileRequ
return nil, gstatus.Errorf(codes.InvalidArgument, "profile name must be provided")
}
callerID, err := callerIdentity(ctx)
if err != nil {
return nil, err
}
resolved, err := s.resolveProfileHandle(msg.ProfileName, callerID)
resolved, err := s.resolveProfileHandle(msg.ProfileName)
if err != nil {
return nil, err
}
@@ -2471,7 +2429,7 @@ func (s *Server) RemoveProfile(ctx context.Context, msg *proto.RemoveProfileRequ
log.Warnf("removing profile %s locally without deregistering it: %v", resolved.ID, err)
}
if err := s.profileManager.RemoveProfile(resolved.ID, callerID); err != nil {
if err := s.profileManager.RemoveProfile(resolved.ID); err != nil {
log.Errorf("failed to remove profile: %v", err)
return nil, fmt.Errorf("failed to remove profile: %w", err)
}
@@ -2506,12 +2464,7 @@ func (s *Server) ClaimProfile(ctx context.Context, msg *proto.ClaimProfileReques
return nil, gstatus.Errorf(codes.InvalidArgument, "%v", err)
}
callerID, err := callerIdentity(ctx)
if err != nil {
return nil, err
}
resolved, err := s.resolveProfileHandle(msg.Handle, callerID)
resolved, err := s.resolveProfileHandle(msg.Handle)
if err != nil {
return nil, err
}
@@ -2595,7 +2548,7 @@ func (s *Server) publishLogLevelChanged(level string) {
)
}
// ListProfiles lists all profiles in the daemon.
// ListProfiles lists all profiles for the caller in the context.
func (s *Server) ListProfiles(ctx context.Context, msg *proto.ListProfilesRequest) (*proto.ListProfilesResponse, error) {
s.mutex.Lock()
defer s.mutex.Unlock()
@@ -2875,15 +2828,10 @@ func (s *Server) authorizeAndPrepareLogin(callerCtx context.Context, msg *proto.
afterLoginPreCheck()
}
callerID, err := callerIdentity(callerCtx)
if err != nil {
return nil, nil, err
}
s.guardedConfigMu.Lock()
defer s.guardedConfigMu.Unlock()
stored, err := s.storedLoginConfig(activeProf, msg, callerID)
stored, err := s.storedLoginConfig(activeProf, msg)
if err != nil {
return nil, nil, err
}
@@ -2907,7 +2855,7 @@ func (s *Server) authorizeAndPrepareLogin(callerCtx context.Context, msg *proto.
}
if msg.ProfileName != nil {
if _, err := s.switchProfileIfNeeded(*msg.ProfileName, callerID, activeProf); err != nil {
if _, err := s.switchProfileIfNeeded(*msg.ProfileName, activeProf); err != nil {
return nil, nil, fmt.Errorf("switch profile: %w", err)
}
}
@@ -2954,6 +2902,12 @@ func (s *Server) SessionHolder() (ipcauth.Principal, bool) {
// This triggers stamping of legacy profiles, and reloads the active profile's
// config so the stamp is visible to SessionHolder.
func (s *Server) OwnsProfile(id ipcauth.Identity, handle string) (bool, error) {
s.profileManager.ClaimDefaultProfileIfNeeded(id)
s.profileManager.ClaimLegacyProfiles(id)
// The daemon's copy of the active profile's config goes stale after a the
// possible profile claims above.
s.reloadActiveConfig()
// Without the active profile there is nothing to fall back to and nothing
// to refresh, so the gate gets a no rather than a guess. The handle is not
// what went wrong here, so the gate is left to refuse in its own words.
@@ -2970,19 +2924,12 @@ func (s *Server) OwnsProfile(id ipcauth.Identity, handle string) (bool, error) {
handle = activeProfile.ID.String()
}
resolved, resolveErr := s.resolveProfileHandle(handle, id)
resolved, resolveErr := s.resolveProfileHandle(handle)
if afterProfileResolve != nil {
afterProfileResolve()
}
// Resolving stamps an owner on every legacy profile the caller can claim,
// not only the one the handle names, so the daemon's copy of the active
// profile's config goes stale whatever the handle was, and whether or not
// resolution succeeded. SessionHolder reads Owners off that copy, so
// refresh it before this answer reaches the gate.
s.reloadActiveConfig()
if resolveErr != nil {
log.Debugf("failed to resolve profile %q: %v", handle, resolveErr)
return false, resolveErr