mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-23 20:25:56 +00:00
7dd5982ace
First-run setup saved the configured administrator but left the router authorizer on its startup identity. This split Settings capability checks: administrator-only panels remained visible while API Access and Pulse Intelligence were denied until restart. Keep the captured authorizer aligned when setup commits the identity, and clear its prior bypass on successful development reset. Cover the exact split with real file-backed RBAC policy and session setup, preserve outsider denial and identity replacement, and synchronise concurrent policy reads. Extension implementations still require independent compatibility review; this is not installation or release acceptance. Change-source: pulse-maintainer
119 lines
3.8 KiB
Go
119 lines
3.8 KiB
Go
package api
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"os"
|
|
"strings"
|
|
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/config"
|
|
internalauth "github.com/rcourtman/pulse-go-rewrite/pkg/auth"
|
|
"github.com/rs/zerolog/log"
|
|
)
|
|
|
|
type firstRunResetResponse struct {
|
|
BootstrapToken string `json:"bootstrapToken"`
|
|
BootstrapTokenPath string `json:"bootstrapTokenPath,omitempty"`
|
|
}
|
|
|
|
func devModeEnabled() bool {
|
|
return os.Getenv("PULSE_DEV") == "true" || strings.EqualFold(os.Getenv("NODE_ENV"), "development")
|
|
}
|
|
|
|
func (r *Router) handleResetFirstRunSecurity(w http.ResponseWriter, req *http.Request) {
|
|
if req.Method != http.MethodPost {
|
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
if !devModeEnabled() {
|
|
http.Error(w, "Development mode required", http.StatusForbidden)
|
|
return
|
|
}
|
|
if !CheckAuth(r.config, w, req) {
|
|
return
|
|
}
|
|
if !ensureSettingsWriteScope(r.config, w, req) {
|
|
return
|
|
}
|
|
|
|
// Prepare the recovery credential before changing any active authentication
|
|
// state. A failure here must leave the caller's current credentials usable so
|
|
// they can repair the data path and retry the reset.
|
|
token, bootstrapCreated, path, err := loadOrCreateBootstrapToken(r.config.DataPath)
|
|
if err != nil {
|
|
log.Error().Err(err).Msg("Failed to recreate bootstrap token during first-run reset")
|
|
http.Error(w, "Failed to recreate bootstrap token", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
rollbackBootstrap := func() {
|
|
if !bootstrapCreated {
|
|
return
|
|
}
|
|
if err := os.Remove(path); err != nil && !os.IsNotExist(err) {
|
|
log.Error().Err(err).Str("token_path", path).Msg("Failed to roll back bootstrap token after first-run reset failure")
|
|
}
|
|
}
|
|
|
|
// Remove restart-time password configuration before committing token
|
|
// revocation. Runtime authentication remains unchanged until both durable
|
|
// operations succeed.
|
|
authEnvSnapshots, err := snapshotAuthEnvFiles(r.config.ConfigPath, r.config.DataPath)
|
|
if err != nil {
|
|
rollbackBootstrap()
|
|
log.Warn().Err(err).Msg("Failed to snapshot auth env files during first-run reset")
|
|
http.Error(w, "Failed to read persisted auth configuration", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
if err := removeAuthEnvFiles(r.config.ConfigPath, r.config.DataPath); err != nil {
|
|
rollbackBootstrap()
|
|
if restoreErr := restoreAuthEnvFiles(authEnvSnapshots); restoreErr != nil {
|
|
log.Error().Err(restoreErr).Msg("Failed to restore auth env files after first-run reset failure")
|
|
}
|
|
log.Warn().Err(err).Msg("Failed to remove auth env files during first-run reset")
|
|
http.Error(w, "Failed to remove persisted auth configuration", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
config.Mu.Lock()
|
|
previousAuthUser := strings.TrimSpace(r.config.AuthUser)
|
|
if r.persistence != nil {
|
|
if err := r.persistence.SaveAPITokens([]config.APITokenRecord{}); err != nil {
|
|
config.Mu.Unlock()
|
|
rollbackBootstrap()
|
|
if restoreErr := restoreAuthEnvFiles(authEnvSnapshots); restoreErr != nil {
|
|
log.Error().Err(restoreErr).Msg("Failed to restore auth env files after API token reset failure")
|
|
}
|
|
log.Warn().Err(err).Msg("Failed to clear persisted API tokens during first-run reset")
|
|
http.Error(w, "Failed to clear persisted API tokens", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
}
|
|
r.config.AuthUser = ""
|
|
r.syncConfiguredAdminAuthorizer()
|
|
r.config.AuthPass = ""
|
|
r.config.APIToken = ""
|
|
r.config.APITokens = nil
|
|
config.Mu.Unlock()
|
|
|
|
for _, key := range []string{
|
|
"PULSE_AUTH_USER",
|
|
"PULSE_AUTH_PASS",
|
|
"REQUIRE_AUTH",
|
|
} {
|
|
_ = os.Unsetenv(key)
|
|
}
|
|
|
|
if previousAuthUser != "" {
|
|
InvalidateUserSessions(previousAuthUser)
|
|
}
|
|
r.clearSession(w, req)
|
|
r.bootstrapTokenHash = internalauth.HashAPIToken(token)
|
|
r.bootstrapTokenPath = path
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_ = json.NewEncoder(w).Encode(firstRunResetResponse{
|
|
BootstrapToken: token,
|
|
BootstrapTokenPath: path,
|
|
})
|
|
}
|