From 50347855828a90b92e840ab5400afee4901bec27 Mon Sep 17 00:00:00 2001 From: chan Date: Wed, 4 Mar 2026 16:10:52 +0900 Subject: [PATCH] fix(backend): fix CSV export authentication by moving role validation inside the handler --- backend/cmd/server/main.go | 2 +- backend/internal/handler/user_handler.go | 58 +++++++++++++++--------- 2 files changed, 38 insertions(+), 22 deletions(-) diff --git a/backend/cmd/server/main.go b/backend/cmd/server/main.go index ef9e72ca..b5dbec68 100644 --- a/backend/cmd/server/main.go +++ b/backend/cmd/server/main.go @@ -643,7 +643,7 @@ func main() { // Admin User Management admin.Get("/users", requireAdmin, userHandler.ListUsers) - admin.Get("/users/export", requireAdmin, userHandler.ExportUsersCSV) + admin.Get("/users/export", userHandler.ExportUsersCSV) // Removed requireAdmin to handle mock role in query param admin.Post("/users", requireAdmin, userHandler.CreateUser) admin.Post("/users/bulk", requireAdmin, userHandler.BulkCreateUsers) admin.Put("/users/bulk", requireAdmin, userHandler.BulkUpdateUsers) diff --git a/backend/internal/handler/user_handler.go b/backend/internal/handler/user_handler.go index 4cfd7989..368c8f8b 100644 --- a/backend/internal/handler/user_handler.go +++ b/backend/internal/handler/user_handler.go @@ -549,30 +549,46 @@ func (h *UserHandler) ExportUsersCSV(c *fiber.Ctx) error { var requesterRole string var manageableSlugs []string - - profile, _ := c.Locals("user_profile").(*domain.UserProfileResponse) - - // [New] Support Role Mocking for Download (which doesn't have custom headers) - if profile == nil { - appEnv := strings.ToLower(os.Getenv("APP_ENV")) - isDev := appEnv == "dev" || appEnv == "development" || appEnv == "" - mockRole := c.Query("x-test-role") - if isDev && mockRole != "" { - slog.Info("🔑 [AUTH] Using mock role from query for export", "role", mockRole) - requesterRole = mockRole - // For tenant_admin, we might need more data, but let's assume super_admin for full export in dev - } else { - return errorJSON(c, fiber.StatusUnauthorized, "invalid session (trace:export_profile)") + var profile *domain.UserProfileResponse + + // [New] Manual profile resolution to support query-param role mocking + // This is needed because browsers cannot send custom headers for direct downloads + mockRole := c.Query("x-test-role") + appEnv := strings.ToLower(os.Getenv("APP_ENV")) + isDev := appEnv == "dev" || appEnv == "development" || appEnv == "" + + if isDev && mockRole != "" { + slog.Info("🔑 [AUTH] Using mock role from query for export", "role", mockRole) + requesterRole = mockRole + // In dev mocking, we might not have a full profile, but we need to know the manageable tenants if it's a tenant_admin + if requesterRole == domain.RoleTenantAdmin { + // Try to get actual profile if possible to get manageableTenants + p, _ := c.Locals("user_profile").(*domain.UserProfileResponse) + if p != nil { + profile = p + } } } else { + // Use real profile from middleware + p, ok := c.Locals("user_profile").(*domain.UserProfileResponse) + if !ok || p == nil { + return errorJSON(c, fiber.StatusUnauthorized, "invalid session (trace:export_auth)") + } + profile = p requesterRole = profile.Role - if requesterRole == domain.RoleTenantAdmin { - for _, t := range profile.ManageableTenants { - manageableSlugs = append(manageableSlugs, strings.ToLower(t.Slug)) - } - if profile.CompanyCode != "" { - manageableSlugs = append(manageableSlugs, strings.ToLower(profile.CompanyCode)) - } + } + + // [New] Access Control: only admin roles can export + if requesterRole != domain.RoleSuperAdmin && requesterRole != domain.RoleTenantAdmin { + return errorJSON(c, fiber.StatusForbidden, "forbidden: insufficient permissions for export") + } + + if profile != nil && requesterRole == domain.RoleTenantAdmin { + for _, t := range profile.ManageableTenants { + manageableSlugs = append(manageableSlugs, strings.ToLower(t.Slug)) + } + if profile.CompanyCode != "" { + manageableSlugs = append(manageableSlugs, strings.ToLower(profile.CompanyCode)) } }