diff --git a/assets/composable/profileStorage.ts b/assets/composable/profileStorage.ts index 8e081a19..0cf83980 100644 --- a/assets/composable/profileStorage.ts +++ b/assets/composable/profileStorage.ts @@ -39,7 +39,7 @@ export function useProfileStorage( } } - if (config.user) { + if (config.user || config.authProvider === "none") { watch( storage, (value) => { @@ -56,6 +56,8 @@ export function useProfileStorage( return value; } }), + }).catch((e) => { + console.error(`Failed to sync ${key} to profile`, e); }); }, { deep: true }, diff --git a/internal/profile/disk.go b/internal/profile/disk.go index 64b55331..21d1dc9c 100644 --- a/internal/profile/disk.go +++ b/internal/profile/disk.go @@ -9,12 +9,12 @@ import ( "os" "path/filepath" - "github.com/amir20/dozzle/internal/auth" "github.com/rs/zerolog/log" ) const ( - profileFilename = "profile.json" + profileFilename = "profile.json" + DefaultUsername = "__default__" ) var errMissingProfileErr = errors.New("Profile file does not exist") @@ -48,6 +48,7 @@ type Profile struct { var dataPath string var mux = &sync.Mutex{} +var errInvalidUsername = errors.New("invalid username: contains path separator or traversal") func init() { path, err := filepath.Abs("./data") @@ -64,10 +65,18 @@ func init() { dataPath = path } -func UpdateFromReader(user auth.User, reader io.Reader) error { +func safePath(username string) (string, error) { + clean := filepath.Base(username) + if clean != username || clean == "." || clean == ".." { + return "", errInvalidUsername + } + return filepath.Join(dataPath, clean), nil +} + +func UpdateFromReader(username string, reader io.Reader) error { mux.Lock() defer mux.Unlock() - existingProfile, err := Load(user) + existingProfile, err := Load(username) if err != nil && err != errMissingProfileErr { log.Error().Err(err).Msg("Unable to load profile. Overwriting it.") } @@ -76,11 +85,14 @@ func UpdateFromReader(user auth.User, reader io.Reader) error { return err } - return save(user, existingProfile) + return save(username, existingProfile) } -func save(user auth.User, profile Profile) error { - path := filepath.Join(dataPath, user.Username) +func save(username string, profile Profile) error { + path, err := safePath(username) + if err != nil { + return err + } if _, err := os.Stat(path); os.IsNotExist(err) { if err := os.Mkdir(path, 0755); err != nil { return err @@ -109,8 +121,11 @@ func save(user auth.User, profile Profile) error { return f.Sync() } -func Load(user auth.User) (Profile, error) { - path := filepath.Join(dataPath, user.Username) +func Load(username string) (Profile, error) { + path, err := safePath(username) + if err != nil { + return Profile{}, err + } profilePath := filepath.Join(path, profileFilename) if _, err := os.Stat(profilePath); os.IsNotExist(err) { diff --git a/internal/web/index.go b/internal/web/index.go index f91288e5..2e695016 100644 --- a/internal/web/index.go +++ b/internal/web/index.go @@ -33,31 +33,38 @@ func (h *handler) executeTemplate(w http.ResponseWriter, req *http.Request) { base = h.config.Base } - hosts := h.hostService.Hosts() - sort.Slice(hosts, func(i, j int) bool { - return hosts[i].Name < hosts[j].Name - }) + user := auth.UserFromContext(req.Context()) + + // Handle unauthorized cases early + if user == nil { + switch h.config.Authorization.Provider { + case FORWARD_PROXY: + log.Error().Msg("Unable to find remote user. Please check your proxy configuration. Expecting headers Remote-Email, Remote-User, Remote-Name.") + log.Debug().Str("url", req.URL.String()).Msg("Dumping all headers for request") + for k, v := range req.Header { + log.Debug().Strs(k, v).Send() + } + http.Error(w, "Unauthorized user", http.StatusUnauthorized) + return + case SIMPLE: + if req.URL.Path != "login" { + log.Debug().Str("url", req.URL.String()).Msg("Redirecting to login page") + http.Redirect(w, req, path.Clean(h.config.Base+"/login")+"?redirectUrl=/"+req.URL.String(), http.StatusTemporaryRedirect) + return + } + } + } config := map[string]interface{}{ "base": base, } - user := auth.UserFromContext(req.Context()) - + // Build full config when authorized (no auth or authenticated user) if h.config.Authorization.Provider == NONE || user != nil { - if user != nil { - config["enableShell"] = h.config.EnableShell && user.Roles.Has(auth.Shell) - config["enableActions"] = h.config.EnableActions && user.Roles.Has(auth.Actions) - config["enableDownload"] = user.Roles.Has(auth.Download) - } else { - config["enableShell"] = h.config.EnableShell - config["enableActions"] = h.config.EnableActions - config["enableDownload"] = true - } - - if h.config.Authorization.Provider == FORWARD_PROXY && strings.TrimSpace(h.config.Authorization.LogoutUrl) != "" { - config["logoutUrl"] = strings.TrimSpace(h.config.Authorization.LogoutUrl) - } + hosts := h.hostService.Hosts() + sort.Slice(hosts, func(i, j int) bool { + return hosts[i].Name < hosts[j].Name + }) config["authProvider"] = h.config.Authorization.Provider config["version"] = h.config.Version @@ -66,27 +73,31 @@ func (h *handler) executeTemplate(w http.ResponseWriter, req *http.Request) { config["hosts"] = hosts config["disableAvatars"] = h.config.DisableAvatars config["releaseCheckMode"] = h.config.ReleaseCheckMode + config["enableShell"] = h.config.EnableShell + config["enableActions"] = h.config.EnableActions + config["enableDownload"] = true + + if user != nil { + config["enableShell"] = h.config.EnableShell && user.Roles.Has(auth.Shell) + config["enableActions"] = h.config.EnableActions && user.Roles.Has(auth.Actions) + config["enableDownload"] = user.Roles.Has(auth.Download) + config["user"] = user + } + + if h.config.Authorization.Provider == FORWARD_PROXY && strings.TrimSpace(h.config.Authorization.LogoutUrl) != "" { + config["logoutUrl"] = strings.TrimSpace(h.config.Authorization.LogoutUrl) + } } + profileUsername := profile.DefaultUsername if user != nil { - if profile, err := profile.Load(*user); err == nil { - config["profile"] = profile - } else { - config["profile"] = struct{}{} - } - config["user"] = user - } else if h.config.Authorization.Provider == FORWARD_PROXY { - log.Error().Msg("Unable to find remote user. Please check your proxy configuration. Expecting headers Remote-Email, Remote-User, Remote-Name.") - log.Debug().Str("url", req.URL.String()).Msg("Dumping all headers for request") - for k, v := range req.Header { - log.Debug().Strs(k, v).Send() - } - http.Error(w, "Unauthorized user", http.StatusUnauthorized) - return - } else if h.config.Authorization.Provider == SIMPLE && req.URL.Path != "login" { - log.Debug().Str("url", req.URL.String()).Msg("Redirecting to login page") - http.Redirect(w, req, path.Clean(h.config.Base+"/login")+"?redirectUrl=/"+req.URL.String(), http.StatusTemporaryRedirect) - return + profileUsername = user.Username + } + + if loadedProfile, err := profile.Load(profileUsername); err == nil { + config["profile"] = loadedProfile + } else { + config["profile"] = struct{}{} } data := map[string]interface{}{ diff --git a/internal/web/profile.go b/internal/web/profile.go index a1921910..53a2b38f 100644 --- a/internal/web/profile.go +++ b/internal/web/profile.go @@ -10,13 +10,12 @@ import ( ) func (h *handler) updateProfile(w http.ResponseWriter, r *http.Request) { - user := auth.UserFromContext(r.Context()) - if user == nil { - http.Error(w, "Unable to find user", http.StatusInternalServerError) - return + username := profile.DefaultUsername + if user := auth.UserFromContext(r.Context()); user != nil { + username = user.Username } - if err := profile.UpdateFromReader(*user, r.Body); err != nil { + if err := profile.UpdateFromReader(username, r.Body); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) log.Error().Err(err).Msg("Failed to update profile") return