mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 02:25:56 +00:00
fix(release): harden handoff auth stores and installer rehearsal harness
This commit is contained in:
@@ -206,6 +206,11 @@ func InitSessionStore(dataPath string) {
|
||||
}
|
||||
}
|
||||
|
||||
func InitPersistentAuthStores(dataPath string) {
|
||||
InitSessionStore(dataPath)
|
||||
InitCSRFStore(dataPath)
|
||||
}
|
||||
|
||||
// GetSessionStore returns the global session store instance
|
||||
func GetSessionStore() *SessionStore {
|
||||
sessionStoreMu.Lock()
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
// Self-guards: returns 404 if the handoff key file does not exist in dataPath,
|
||||
// meaning this is not a cloud-managed tenant.
|
||||
func HandleCloudHandoff(dataPath string) http.HandlerFunc {
|
||||
InitPersistentAuthStores(dataPath)
|
||||
replay := &jtiReplayStore{configDir: dataPath}
|
||||
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
@@ -281,6 +281,7 @@ func normalizeHandoffEmail(email string) string {
|
||||
// this returns a success payload instead of redirecting.
|
||||
func HandleHandoffExchange(configDir string) http.HandlerFunc {
|
||||
configDir = filepath.Clean(configDir)
|
||||
InitPersistentAuthStores(configDir)
|
||||
keyPath := filepath.Join(configDir, "secrets", "handoff.key")
|
||||
replay := &jtiReplayStore{configDir: configDir}
|
||||
|
||||
|
||||
@@ -14,6 +14,8 @@ import (
|
||||
)
|
||||
|
||||
func TestHandleCloudHandoffRejectsReplay(t *testing.T) {
|
||||
resetPersistentAuthStoresForTests()
|
||||
t.Cleanup(resetPersistentAuthStoresForTests)
|
||||
dataPath := t.TempDir()
|
||||
key := []byte("0123456789abcdef0123456789abcdef")
|
||||
if err := os.WriteFile(filepath.Join(dataPath, cloudauth.HandoffKeyFile), key, 0o600); err != nil {
|
||||
@@ -54,6 +56,8 @@ func TestHandleCloudHandoffRejectsReplay(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestHandleCloudHandoffSetsTenantOrgCookie(t *testing.T) {
|
||||
resetPersistentAuthStoresForTests()
|
||||
t.Cleanup(resetPersistentAuthStoresForTests)
|
||||
dataPath := t.TempDir()
|
||||
key := []byte("0123456789abcdef0123456789abcdef")
|
||||
if err := os.WriteFile(filepath.Join(dataPath, cloudauth.HandoffKeyFile), key, 0o600); err != nil {
|
||||
@@ -91,6 +95,8 @@ func TestHandleCloudHandoffSetsTenantOrgCookie(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestHandleCloudHandoffRejectsInvalidTenantID(t *testing.T) {
|
||||
resetPersistentAuthStoresForTests()
|
||||
t.Cleanup(resetPersistentAuthStoresForTests)
|
||||
dataPath := t.TempDir()
|
||||
key := []byte("0123456789abcdef0123456789abcdef")
|
||||
if err := os.WriteFile(filepath.Join(dataPath, cloudauth.HandoffKeyFile), key, 0o600); err != nil {
|
||||
@@ -117,6 +123,8 @@ func TestHandleCloudHandoffRejectsInvalidTenantID(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestHandleCloudHandoffLowercasesSessionEmailIdentity(t *testing.T) {
|
||||
resetPersistentAuthStoresForTests()
|
||||
t.Cleanup(resetPersistentAuthStoresForTests)
|
||||
dataPath := t.TempDir()
|
||||
key := []byte("0123456789abcdef0123456789abcdef")
|
||||
if err := os.WriteFile(filepath.Join(dataPath, cloudauth.HandoffKeyFile), key, 0o600); err != nil {
|
||||
|
||||
@@ -30,6 +30,7 @@ type CSRFTokenStore struct {
|
||||
dataPath string
|
||||
saveTicker *time.Ticker
|
||||
stopChan chan bool
|
||||
workerDone chan struct{}
|
||||
stopOnce sync.Once
|
||||
}
|
||||
|
||||
@@ -112,9 +113,10 @@ func InitCSRFStore(dataPath string) {
|
||||
|
||||
oldStore := csrfStore
|
||||
csrfStore = &CSRFTokenStore{
|
||||
tokens: make(map[string]*CSRFToken),
|
||||
dataPath: newDataPath,
|
||||
stopChan: make(chan bool),
|
||||
tokens: make(map[string]*CSRFToken),
|
||||
dataPath: newDataPath,
|
||||
stopChan: make(chan bool),
|
||||
workerDone: make(chan struct{}),
|
||||
}
|
||||
csrfStoreDataPath = newDataPath
|
||||
|
||||
@@ -154,6 +156,9 @@ func (c *CSRFTokenStore) Shutdown() {
|
||||
default:
|
||||
}
|
||||
})
|
||||
if c.workerDone != nil {
|
||||
<-c.workerDone
|
||||
}
|
||||
}
|
||||
|
||||
func resetCSRFStoreForTests() {
|
||||
@@ -169,6 +174,7 @@ func resetCSRFStoreForTests() {
|
||||
|
||||
// backgroundWorker handles periodic saves and cleanup
|
||||
func (c *CSRFTokenStore) backgroundWorker() {
|
||||
defer close(c.workerDone)
|
||||
for {
|
||||
select {
|
||||
case <-c.saveTicker.C:
|
||||
|
||||
@@ -20,6 +20,7 @@ type SessionStore struct {
|
||||
dataPath string
|
||||
saveTicker *time.Ticker
|
||||
stopChan chan bool
|
||||
workerDone chan struct{}
|
||||
stopOnce sync.Once
|
||||
crypto *crypto.CryptoManager
|
||||
}
|
||||
@@ -150,10 +151,11 @@ func NewSessionStore(dataPath string) *SessionStore {
|
||||
}
|
||||
|
||||
store := &SessionStore{
|
||||
sessions: make(map[string]*SessionData),
|
||||
dataPath: dataPath,
|
||||
stopChan: make(chan bool),
|
||||
crypto: cm,
|
||||
sessions: make(map[string]*SessionData),
|
||||
dataPath: dataPath,
|
||||
stopChan: make(chan bool),
|
||||
workerDone: make(chan struct{}),
|
||||
crypto: cm,
|
||||
}
|
||||
|
||||
// Load existing sessions from disk
|
||||
@@ -179,10 +181,14 @@ func (s *SessionStore) Shutdown() {
|
||||
default:
|
||||
}
|
||||
})
|
||||
if s.workerDone != nil {
|
||||
<-s.workerDone
|
||||
}
|
||||
}
|
||||
|
||||
// backgroundWorker handles periodic saves and cleanup
|
||||
func (s *SessionStore) backgroundWorker() {
|
||||
defer close(s.workerDone)
|
||||
for {
|
||||
select {
|
||||
case <-s.saveTicker.C:
|
||||
|
||||
@@ -776,6 +776,32 @@ func extractRootInstallShellFunction(t *testing.T, name string) string {
|
||||
return string(match)
|
||||
}
|
||||
|
||||
func extractSetupAutoUpdatesShellFunctions(t *testing.T) string {
|
||||
t.Helper()
|
||||
|
||||
return extractRootInstallShellFunction(t, "selected_update_channel") + "\n" +
|
||||
extractRootInstallShellFunction(t, "repo_web_url") + "\n" +
|
||||
extractRootInstallShellFunction(t, "configure_auto_update_script_repo") + "\n" +
|
||||
extractRootInstallShellFunction(t, "setup_auto_updates")
|
||||
}
|
||||
|
||||
func prepareAutoUpdatePaths(t *testing.T, tmpDir string) (string, string, string) {
|
||||
t.Helper()
|
||||
|
||||
autoUpdateDest := filepath.Join(tmpDir, "bin", "pulse-auto-update.sh")
|
||||
servicePath := filepath.Join(tmpDir, "systemd", "pulse-update.service")
|
||||
timerPath := filepath.Join(tmpDir, "systemd", "pulse-update.timer")
|
||||
|
||||
if err := os.MkdirAll(filepath.Dir(autoUpdateDest), 0755); err != nil {
|
||||
t.Fatalf("mkdir auto-update dest dir: %v", err)
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(servicePath), 0755); err != nil {
|
||||
t.Fatalf("mkdir systemd dir: %v", err)
|
||||
}
|
||||
|
||||
return autoUpdateDest, servicePath, timerPath
|
||||
}
|
||||
|
||||
func extractAutoUpdateFunction(t *testing.T, name string) string {
|
||||
t.Helper()
|
||||
|
||||
@@ -1620,16 +1646,13 @@ func TestSelectedUpdateChannelTreatsPrereleaseVersionAsRC(t *testing.T) {
|
||||
func TestSetupAutoUpdatesCreatesSystemJSONWithSelectedChannel(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
configDir := filepath.Join(tmpDir, "config")
|
||||
systemdDir := filepath.Join(tmpDir, "systemd")
|
||||
installDir := filepath.Join(tmpDir, "install")
|
||||
autoUpdateSrc := filepath.Join(installDir, "scripts", "pulse-auto-update.sh")
|
||||
autoUpdateDest, servicePath, timerPath := prepareAutoUpdatePaths(t, tmpDir)
|
||||
|
||||
if err := os.MkdirAll(configDir, 0755); err != nil {
|
||||
t.Fatalf("mkdir config dir: %v", err)
|
||||
}
|
||||
if err := os.MkdirAll(systemdDir, 0755); err != nil {
|
||||
t.Fatalf("mkdir systemd dir: %v", err)
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(autoUpdateSrc), 0755); err != nil {
|
||||
t.Fatalf("mkdir auto-update src dir: %v", err)
|
||||
}
|
||||
@@ -1640,6 +1663,9 @@ func TestSetupAutoUpdatesCreatesSystemJSONWithSelectedChannel(t *testing.T) {
|
||||
script := `
|
||||
CONFIG_DIR="` + configDir + `"
|
||||
INSTALL_DIR="` + installDir + `"
|
||||
PULSE_AUTO_UPDATE_DEST="` + autoUpdateDest + `"
|
||||
PULSE_UPDATE_SERVICE_PATH="` + servicePath + `"
|
||||
PULSE_UPDATE_TIMER_PATH="` + timerPath + `"
|
||||
FORCE_CHANNEL="rc"
|
||||
UPDATE_CHANNEL=""
|
||||
GITHUB_REPO="rcourtman/Pulse"
|
||||
@@ -1653,8 +1679,7 @@ func TestSetupAutoUpdatesCreatesSystemJSONWithSelectedChannel(t *testing.T) {
|
||||
chown() { :; }
|
||||
cat() { command cat "$@"; }
|
||||
mkdir() { command mkdir "$@"; }
|
||||
` + extractRootInstallShellFunction(t, "selected_update_channel") + `
|
||||
` + extractRootInstallShellFunction(t, "setup_auto_updates") + `
|
||||
` + extractSetupAutoUpdatesShellFunctions(t) + `
|
||||
setup_auto_updates
|
||||
`
|
||||
|
||||
@@ -1728,10 +1753,7 @@ func TestSetupAutoUpdatesConfiguresInstalledAutoUpdateRepo(t *testing.T) {
|
||||
rm() { command rm "$@"; }
|
||||
awk() { command awk "$@"; }
|
||||
mktemp() { command mktemp "$@"; }
|
||||
` + extractRootInstallShellFunction(t, "selected_update_channel") + `
|
||||
` + extractRootInstallShellFunction(t, "repo_web_url") + `
|
||||
` + extractRootInstallShellFunction(t, "configure_auto_update_script_repo") + `
|
||||
` + extractRootInstallShellFunction(t, "setup_auto_updates") + `
|
||||
` + extractSetupAutoUpdatesShellFunctions(t) + `
|
||||
setup_auto_updates
|
||||
`
|
||||
|
||||
@@ -1779,6 +1801,7 @@ func TestSetupAutoUpdatesTreatsPrereleaseVersionAsRCChannel(t *testing.T) {
|
||||
configDir := filepath.Join(tmpDir, "config")
|
||||
installDir := filepath.Join(tmpDir, "install")
|
||||
autoUpdateSrc := filepath.Join(installDir, "scripts", "pulse-auto-update.sh")
|
||||
autoUpdateDest, servicePath, timerPath := prepareAutoUpdatePaths(t, tmpDir)
|
||||
|
||||
if err := os.MkdirAll(configDir, 0755); err != nil {
|
||||
t.Fatalf("mkdir config dir: %v", err)
|
||||
@@ -1793,6 +1816,9 @@ func TestSetupAutoUpdatesTreatsPrereleaseVersionAsRCChannel(t *testing.T) {
|
||||
script := `
|
||||
CONFIG_DIR="` + configDir + `"
|
||||
INSTALL_DIR="` + installDir + `"
|
||||
PULSE_AUTO_UPDATE_DEST="` + autoUpdateDest + `"
|
||||
PULSE_UPDATE_SERVICE_PATH="` + servicePath + `"
|
||||
PULSE_UPDATE_TIMER_PATH="` + timerPath + `"
|
||||
FORCE_CHANNEL=""
|
||||
FORCE_VERSION="v1.2.3-rc.4"
|
||||
UPDATE_CHANNEL=""
|
||||
@@ -1803,8 +1829,7 @@ func TestSetupAutoUpdatesTreatsPrereleaseVersionAsRCChannel(t *testing.T) {
|
||||
safe_systemctl() { :; }
|
||||
systemctl() { return 0; }
|
||||
chown() { :; }
|
||||
` + extractRootInstallShellFunction(t, "selected_update_channel") + `
|
||||
` + extractRootInstallShellFunction(t, "setup_auto_updates") + `
|
||||
` + extractSetupAutoUpdatesShellFunctions(t) + `
|
||||
setup_auto_updates
|
||||
`
|
||||
|
||||
@@ -1831,6 +1856,7 @@ func TestSetupAutoUpdatesPreservesRCChannelWhenUpdatingExistingConfig(t *testing
|
||||
configDir := filepath.Join(tmpDir, "config")
|
||||
installDir := filepath.Join(tmpDir, "install")
|
||||
autoUpdateSrc := filepath.Join(installDir, "scripts", "pulse-auto-update.sh")
|
||||
autoUpdateDest, servicePath, timerPath := prepareAutoUpdatePaths(t, tmpDir)
|
||||
|
||||
if err := os.MkdirAll(configDir, 0755); err != nil {
|
||||
t.Fatalf("mkdir config dir: %v", err)
|
||||
@@ -1848,6 +1874,9 @@ func TestSetupAutoUpdatesPreservesRCChannelWhenUpdatingExistingConfig(t *testing
|
||||
script := `
|
||||
CONFIG_DIR="` + configDir + `"
|
||||
INSTALL_DIR="` + installDir + `"
|
||||
PULSE_AUTO_UPDATE_DEST="` + autoUpdateDest + `"
|
||||
PULSE_UPDATE_SERVICE_PATH="` + servicePath + `"
|
||||
PULSE_UPDATE_TIMER_PATH="` + timerPath + `"
|
||||
FORCE_CHANNEL=""
|
||||
UPDATE_CHANNEL=""
|
||||
GITHUB_REPO="rcourtman/Pulse"
|
||||
@@ -1858,8 +1887,7 @@ func TestSetupAutoUpdatesPreservesRCChannelWhenUpdatingExistingConfig(t *testing
|
||||
systemctl() { return 0; }
|
||||
command -v jq >/dev/null 2>&1 || true
|
||||
chown() { :; }
|
||||
` + extractRootInstallShellFunction(t, "selected_update_channel") + `
|
||||
` + extractRootInstallShellFunction(t, "setup_auto_updates") + `
|
||||
` + extractSetupAutoUpdatesShellFunctions(t) + `
|
||||
setup_auto_updates
|
||||
`
|
||||
|
||||
|
||||
Reference in New Issue
Block a user