This commit is contained in:
2026-08-14 01:38:34 +02:00
parent a9395585f8
commit dc9ea628c1
12 changed files with 163 additions and 140 deletions
+54 -52
View File
@@ -24,12 +24,10 @@ func isLocalDemoEmail(email string) bool {
}
}
// resolveDevImpersonationActor returns the privileged actor allowed to drive non-prod
// user switching: the current full admin/demo user, or the stored impersonator.
// resolveImpersonationActor returns the privileged actor allowed to drive user
// switching: platform staff_role=admin (any env), plus non-prod full admin / demo
// users, or the stored impersonator when still privileged.
func (s *Server) resolveDevImpersonationActor(ctx context.Context) (actorID uuid.UUID, ok bool, err error) {
if s.Config.IsProduction() {
return uuid.Nil, false, nil
}
uid, hasUID := UserIDFromContext(ctx)
if !hasUID || uid == uuid.Nil {
return uuid.Nil, false, nil
@@ -38,15 +36,9 @@ func (s *Server) resolveDevImpersonationActor(ctx context.Context) (actorID uuid
return uuid.Nil, false, errors.New("auth unavailable")
}
access, err := s.checkStaffAccess(ctx, uid)
if err != nil {
if canImpersonate, err := s.userMayImpersonate(ctx, uid); err != nil {
return uuid.Nil, false, err
}
if access.FullAdmin {
return uid, true, nil
}
user, err := s.Auth.GetUser(ctx, uid)
if err == nil && isLocalDemoEmail(user.Email) {
} else if canImpersonate {
return uid, true, nil
}
@@ -58,27 +50,44 @@ func (s *Server) resolveDevImpersonationActor(ctx context.Context) (actorID uuid
if err != nil || impID == uuid.Nil {
return uuid.Nil, false, nil
}
impAccess, err := s.checkStaffAccess(ctx, impID)
if err != nil {
if canImpersonate, err := s.userMayImpersonate(ctx, impID); err != nil {
return uuid.Nil, false, err
}
if impAccess.FullAdmin {
return impID, true, nil
}
impUser, err := s.Auth.GetUser(ctx, impID)
if err == nil && isLocalDemoEmail(impUser.Email) {
} else if canImpersonate {
return impID, true, nil
}
return uuid.Nil, false, nil
}
// handleAdminDevSetPassword sets a known local password for any active user.
// Blocked in production. Intended for @legacy.local migrated accounts (invite emails skip those).
func (s *Server) handleAdminDevSetPassword(w http.ResponseWriter, r *http.Request) {
if s.Config.IsProduction() {
Error(w, http.StatusNotFound, "not found")
return
// userMayImpersonate is true for platform staff_role=admin in any environment.
// Non-production also allows other full admins and local demo accounts.
func (s *Server) userMayImpersonate(ctx context.Context, userID uuid.UUID) (bool, error) {
access, err := s.checkStaffAccess(ctx, userID)
if err != nil {
return false, err
}
if access.FullAdmin && access.Role == auth.StaffRoleAdmin {
return true, nil
}
if s.Config.IsProduction() {
return false, nil
}
if access.FullAdmin {
return true, nil
}
if s.Auth == nil {
return false, nil
}
user, err := s.Auth.GetUser(ctx, userID)
if err != nil {
return false, nil
}
return isLocalDemoEmail(user.Email), nil
}
// handleAdminDevSetPassword force-sets a password for any active user.
// Platform admin only (route). Production requires an explicit password (no default)
// so ops can unlock @legacy.local / fake-email accounts that cannot receive invites.
func (s *Server) handleAdminDevSetPassword(w http.ResponseWriter, r *http.Request) {
if s.Auth == nil {
Error(w, http.StatusServiceUnavailable, "auth unavailable")
return
@@ -92,8 +101,12 @@ func (s *Server) handleAdminDevSetPassword(w http.ResponseWriter, r *http.Reques
Password string `json:"password"`
}
_ = DecodeJSONOptional(r, &body)
password := body.Password
if strings.TrimSpace(password) == "" {
password := strings.TrimSpace(body.Password)
if password == "" {
if s.Config.IsProduction() {
Error(w, http.StatusBadRequest, "password is required")
return
}
password = defaultDevPassword
}
if len(password) < 8 {
@@ -117,20 +130,21 @@ func (s *Server) handleAdminDevSetPassword(w http.ResponseWriter, r *http.Reques
LogAndError(w, http.StatusInternalServerError, "could not set password", err)
return
}
hint := "Password set. Sign in with this email and the password you provided."
if !s.Config.IsProduction() {
hint = "Password set for local login. Omit body.password to use the built-in local default."
}
JSON(w, http.StatusOK, map[string]any{
"ok": true,
"user_id": id,
"email": user.Email,
"hint": "Password set for local login. Omit body.password to use the built-in local default.",
"hint": hint,
})
}
// handleAdminDevImpersonate swaps the current session to the target user (non-production only).
// handleAdminDevImpersonate swaps the current session to the target user so the
// operator sees that user's tenant membership and plan (not platform staff chrome).
func (s *Server) handleAdminDevImpersonate(w http.ResponseWriter, r *http.Request) {
if s.Config.IsProduction() {
Error(w, http.StatusNotFound, "not found")
return
}
if s.Auth == nil {
Error(w, http.StatusServiceUnavailable, "auth unavailable")
return
@@ -191,10 +205,6 @@ func (s *Server) handleAdminDevImpersonate(w http.ResponseWriter, r *http.Reques
// handleAdminDevStopImpersonate restores the session to the original admin/demo actor.
func (s *Server) handleAdminDevStopImpersonate(w http.ResponseWriter, r *http.Request) {
if s.Config.IsProduction() {
Error(w, http.StatusNotFound, "not found")
return
}
if s.Auth == nil {
Error(w, http.StatusServiceUnavailable, "auth unavailable")
return
@@ -219,14 +229,10 @@ func (s *Server) handleAdminDevStopImpersonate(w http.ResponseWriter, r *http.Re
return
}
if impID != actorID {
// Prefer the stored impersonator when it is still the privileged actor.
impAccess, aerr := s.checkStaffAccess(r.Context(), impID)
if aerr != nil || !impAccess.FullAdmin {
impUser, uerr := s.Auth.GetUser(r.Context(), impID)
if uerr != nil || !isLocalDemoEmail(impUser.Email) {
Error(w, http.StatusForbidden, "user switch not allowed")
return
}
canImp, ierr := s.userMayImpersonate(r.Context(), impID)
if ierr != nil || !canImp {
Error(w, http.StatusForbidden, "user switch not allowed")
return
}
}
user, err := s.Auth.GetUser(r.Context(), impID)
@@ -393,12 +399,8 @@ func enrichSwitchableUser(u *switchableUserRow, legacyCompanyID string) {
}
// handleAdminDevListSwitchableUsers lists active users with a preferred company label
// for the header user-switch dropdown (non-production only).
// for the header user-switch dropdown (platform staff_role=admin, or non-prod demo).
func (s *Server) handleAdminDevListSwitchableUsers(w http.ResponseWriter, r *http.Request) {
if s.Config.IsProduction() {
Error(w, http.StatusNotFound, "not found")
return
}
if s.Pool == nil {
Error(w, http.StatusServiceUnavailable, "database unavailable")
return