package release import ( "encoding/json" "fmt" "slices" ) // The update settings vocabulary (instance update settings spec §3). const ( UpdatesAuto = "auto" UpdatesManual = "manual" ControlUpdates = "updates" ControlChannel = "channel" ControlMajor = "major" ControlPin = "pin" ) // UpdateSettings are one instance's EFFECTIVE settings: the record's explicit // values over the app's defaults, resolved by reconcile // (provision.EffectiveUpdates) and carried to root in the registry's // update_settings column. A preference, not a security boundary (spec §2 // decision 5): root still installs only builds §4.1 authorizes. type UpdateSettings struct { Updates string `json:"updates"` Channel string `json:"channel"` Major *int `json:"major,omitempty"` Pin string `json:"pin,omitempty"` InstallRequest uint64 `json:"install_request,omitempty"` InstallFastTrack bool `json:"install_fast_track,omitempty"` Control []string `json:"control,omitempty"` } // ParseUpdateSettings reads the column. "" is every row before this feature, // and reads as auto: today's adoption, byte for byte. func ParseUpdateSettings(col string) (UpdateSettings, error) { if col == "" { return UpdateSettings{Updates: UpdatesAuto}, nil } var u UpdateSettings if err := json.Unmarshal([]byte(col), &u); err != nil { return UpdateSettings{}, fmt.Errorf("update settings: %v", err) } if u.Updates != UpdatesAuto && u.Updates != UpdatesManual { return UpdateSettings{}, fmt.Errorf("update settings: updates %q", u.Updates) } if u.Major != nil && *u.Major < 0 { return UpdateSettings{}, fmt.Errorf("update settings: major %d", *u.Major) } if u.Pin != "" && (!ValidVersion(u.Pin) || u.Pin == "dev") { return UpdateSettings{}, fmt.Errorf("update settings: pin %q", u.Pin) } if u.Channel != "" && !ValidChannel(u.Channel) { return UpdateSettings{}, fmt.Errorf("update settings: channel %q", u.Channel) } if !ValidControl(u.Control) { return UpdateSettings{}, fmt.Errorf("update settings: control %q", u.Control) } return u, nil } // Column is the canonical JSON reconcile writes. Default settings are "": // a row is byte-identical to before this feature until someone sets // something. func (u UpdateSettings) Column() string { if u.Default() && len(u.Control) == 0 { return "" } b, _ := json.Marshal(u) return string(b) } // Default reports today's behaviour: auto, no major hold, no pin, no request. func (u UpdateSettings) Default() bool { return (u.Updates == "" || u.Updates == UpdatesAuto) && u.Major == nil && u.Pin == "" && u.InstallRequest == 0 } // ValidControl accepts a set of setting names an instance may change itself. func ValidControl(names []string) bool { seen := map[string]bool{} for _, n := range names { if seen[n] || !slices.Contains([]string{ControlUpdates, ControlChannel, ControlMajor, ControlPin}, n) { return false } seen[n] = true } return true } // ParseUpdateSettingsLenient is the box's read of the column (root's pass, // the agent's passes and the held wake check): ParseUpdateSettings with // Control names this binary does not know, and repeats, dropped first. // Control is the console's business (who may change what); the box never // acts on it, so a name a newer console or policy added must not make the // whole column unreadable and drop the hold it carries (ruling // R-SETTINGS-UNREADABLE). Every other field is validated as strictly as // ParseUpdateSettings does; the console keeps the strict parse. func ParseUpdateSettingsLenient(col string) (UpdateSettings, error) { if col == "" { return ParseUpdateSettings(col) } var u UpdateSettings if err := json.Unmarshal([]byte(col), &u); err != nil { return UpdateSettings{}, fmt.Errorf("update settings: %v", err) } var known []string for _, n := range u.Control { if ValidControl([]string{n}) && !slices.Contains(known, n) { known = append(known, n) } } u.Control = known b, err := json.Marshal(u) if err != nil { return UpdateSettings{}, fmt.Errorf("update settings: %v", err) } return ParseUpdateSettings(string(b)) }