package api import ( "encoding/json" "net/http" "strconv" "archivdms/internal/audit" "archivdms/internal/userstore" ) func (s *Server) handleListUsers(w http.ResponseWriter, r *http.Request) { sess := sessionFromCtx(r.Context()) var users []*userstore.User var err error if sess.TenantID != nil { users, err = s.users.ListByTenant(r.Context(), *sess.TenantID) } else { users, err = s.users.List("") } if err != nil { writeError(w, http.StatusInternalServerError, "list users failed") return } writeJSON(w, http.StatusOK, users) } type createUserRequest struct { Username string `json:"username"` Email string `json:"email"` Password string `json:"password"` Role string `json:"role"` // TenantID is only ever evaluated for a superadmin caller (see // handleCreateUser). For any other role it is silently ignored and the // caller's own sess.TenantID is enforced instead — this is a deliberate // IDOR guard: a domain_admin must never be able to steer a created user // into a tenant other than their own by sending a different tenant_id. TenantID *int64 `json:"tenant_id"` } func (s *Server) handleCreateUser(w http.ResponseWriter, r *http.Request) { sess := sessionFromCtx(r.Context()) var req createUserRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "invalid request body") return } if req.Role == "" { req.Role = userstore.RoleUser } // Tenant assignment is rollenabhängig (security-critical, see plan): // - superadmin: may set tenant_id explicitly from the request body, // including nil for another tenant-less superadmin. // - everyone else (domain_admin, user): tenant_id is ALWAYS hard-forced // to the caller's own sess.TenantID; any tenant_id in the request // body is completely ignored, not merely validated, to close the // IDOR hole where a domain_admin could otherwise create a user in a // tenant they don't administer. tenantID := sess.TenantID if sess.Role == userstore.RoleSuperAdmin { tenantID = req.TenantID } user, err := s.users.Create(userstore.CreateUserRequest{ Username: req.Username, Email: req.Email, Password: req.Password, Role: req.Role, TenantID: tenantID, }) if err != nil { s.audlog.Log(audit.Entry{EventType: audit.EventUserMgmt, Username: sess.Username, TenantID: sess.TenantID, Success: false, Detail: "create_user_failed"}) writeError(w, http.StatusBadRequest, "create user failed") return } s.audlog.Log(audit.Entry{EventType: audit.EventUserMgmt, Username: sess.Username, TenantID: sess.TenantID, Success: true, Detail: "user_created:" + user.Username}) writeJSON(w, http.StatusCreated, user) } type updateUserRequest struct { Email *string `json:"email"` Role *string `json:"role"` Active *bool `json:"active"` Password *string `json:"password"` } func (s *Server) handleUpdateUser(w http.ResponseWriter, r *http.Request) { sess := sessionFromCtx(r.Context()) id, err := strconv.ParseInt(r.PathValue("id"), 10, 64) if err != nil { writeError(w, http.StatusBadRequest, "invalid user id") return } var req updateUserRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "invalid request body") return } user, err := s.users.Update(id, userstore.UpdateUserRequest{ Email: req.Email, Role: req.Role, Active: req.Active, Password: req.Password, }) if err != nil { s.audlog.Log(audit.Entry{EventType: audit.EventUserMgmt, Username: sess.Username, TenantID: sess.TenantID, Success: false, Detail: "update_user_failed"}) writeError(w, http.StatusBadRequest, "update user failed") return } s.audlog.Log(audit.Entry{EventType: audit.EventUserMgmt, Username: sess.Username, TenantID: sess.TenantID, Success: true, Detail: "user_updated:" + user.Username}) writeJSON(w, http.StatusOK, user) } func (s *Server) handleDeleteUser(w http.ResponseWriter, r *http.Request) { sess := sessionFromCtx(r.Context()) id, err := strconv.ParseInt(r.PathValue("id"), 10, 64) if err != nil { writeError(w, http.StatusBadRequest, "invalid user id") return } if err := s.users.Delete(id); err != nil { s.audlog.Log(audit.Entry{EventType: audit.EventUserMgmt, Username: sess.Username, TenantID: sess.TenantID, Success: false, Detail: "delete_user_failed"}) writeError(w, http.StatusBadRequest, "delete user failed") return } s.audlog.Log(audit.Entry{EventType: audit.EventUserMgmt, Username: sess.Username, TenantID: sess.TenantID, Success: true, Detail: "user_deleted"}) writeJSON(w, http.StatusOK, map[string]string{"status": "deleted"}) }