package api import ( "encoding/json" "fmt" "net/http" "strconv" "archivmail/internal/audit" "archivmail/internal/userstore" ) func (s *Server) handleListUsers(w http.ResponseWriter, r *http.Request) { tenantID := tenantFromCtx(r.Context()) var ( users []*userstore.User err error ) if tenantID != nil { users, err = s.users.ListByTenant(r.Context(), *tenantID) } else { users, err = s.users.List("") } if err != nil { writeError(w, http.StatusInternalServerError, "failed to list users") return } type userResp struct { ID int64 `json:"id"` Username string `json:"username"` Email string `json:"email"` Role string `json:"role"` Active bool `json:"active"` TenantID *int64 `json:"tenant_id,omitempty"` } resp := make([]userResp, 0, len(users)) for _, u := range users { resp = append(resp, userResp{ ID: u.ID, Username: u.Username, Email: u.Email, Role: u.Role, Active: u.Active, TenantID: u.TenantID, }) } writeJSON(w, http.StatusOK, resp) } func (s *Server) handleCreateUser(w http.ResponseWriter, r *http.Request) { var req struct { Username string `json:"username"` Email string `json:"email"` Password string `json:"password"` Role string `json:"role"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "invalid request body") return } // SEC-01: Privilege escalation check — caller must not assign a role // at or above their own level. sess := sessionFromCtx(r.Context()) if roleLevel(req.Role) >= roleLevel(sess.Role) { writeError(w, http.StatusForbidden, "insufficient privileges to assign this role") return } // SEC-02: Tenant isolation — non-superadmin users can only create users // within their own tenant. var tenantID *int64 if sess.TenantID != nil { tenantID = sess.TenantID } // PROJ-29: Enforce max_users quota before creating a new user. if tenantID != nil && s.tenantStore != nil { quota, qErr := s.tenantStore.GetQuota(r.Context(), *tenantID) if qErr == nil && quota.MaxUsers != nil { usage, uErr := s.tenantStore.GetUsage(r.Context(), *tenantID) if uErr == nil && int(usage.UserCount) >= *quota.MaxUsers { writeError(w, http.StatusPaymentRequired, "user quota exceeded") return } } } 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.logger.Error("create user failed", "err", err) writeError(w, http.StatusBadRequest, "user creation failed") return } s.audlog.Log(audit.Entry{ EventType: audit.EventUserMgmt, Username: sess.Username, TenantID: sess.TenantID, IPAddress: s.remoteIP(r), Detail: "created user: " + user.Username, Success: true, }) writeJSON(w, http.StatusCreated, map[string]interface{}{ "id": user.ID, "username": user.Username, "email": user.Email, "role": user.Role, "active": user.Active, }) } func (s *Server) handleUpdateUser(w http.ResponseWriter, r *http.Request) { id, err := strconv.ParseInt(r.PathValue("id"), 10, 64) if err != nil { writeError(w, http.StatusBadRequest, "invalid user id") return } var req struct { Email *string `json:"email"` Role *string `json:"role"` Active *bool `json:"active"` Password *string `json:"password"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "invalid request body") return } sess := sessionFromCtx(r.Context()) // SEC-02: Tenant isolation — load target user and verify same tenant. target, err := s.users.GetByID(id) if err != nil { writeError(w, http.StatusNotFound, "user not found") return } if sess.TenantID != nil { if target.TenantID == nil || *target.TenantID != *sess.TenantID { writeError(w, http.StatusForbidden, "access denied") return } } // SEC-01: Privilege escalation check — caller must not assign a role // at or above their own level, and must not modify users at or above // their own level. if roleLevel(target.Role) >= roleLevel(sess.Role) { writeError(w, http.StatusForbidden, "insufficient privileges to modify this user") return } if req.Role != nil && roleLevel(*req.Role) >= roleLevel(sess.Role) { writeError(w, http.StatusForbidden, "insufficient privileges to assign this role") return } updated, err := s.users.Update(id, userstore.UpdateUserRequest{ Email: req.Email, Role: req.Role, Active: req.Active, Password: req.Password, }) if err != nil { s.logger.Error("update user failed", "err", err) writeError(w, http.StatusBadRequest, "user update failed") return } s.audlog.Log(audit.Entry{ EventType: audit.EventUserMgmt, Username: sess.Username, TenantID: sess.TenantID, IPAddress: s.remoteIP(r), Detail: fmt.Sprintf("updated user %d", id), Success: true, }) writeJSON(w, http.StatusOK, map[string]interface{}{ "id": updated.ID, "username": updated.Username, "email": updated.Email, "role": updated.Role, "active": updated.Active, }) } func (s *Server) handleDeleteUser(w http.ResponseWriter, r *http.Request) { id, err := strconv.ParseInt(r.PathValue("id"), 10, 64) if err != nil { writeError(w, http.StatusBadRequest, "invalid user id") return } // Fetch user info before deletion for audit log and IMAP cleanup target, err := s.users.GetByID(id) if err != nil { writeError(w, http.StatusNotFound, "user not found") return } // SEC-02: Tenant isolation — domain_admin can only delete users in their own tenant. sess := sessionFromCtx(r.Context()) if sess.TenantID != nil { if target.TenantID == nil || *target.TenantID != *sess.TenantID { writeError(w, http.StatusForbidden, "access denied") return } } // SEC-01: Cannot delete users at or above own privilege level. if roleLevel(target.Role) >= roleLevel(sess.Role) { writeError(w, http.StatusForbidden, "insufficient privileges to delete this user") return } if err := s.users.DeleteSafe(id); err != nil { if err.Error() == "userstore: cannot delete last admin" { writeError(w, http.StatusConflict, "cannot delete the last active admin") return } s.logger.Error("delete user failed", "err", err) writeError(w, http.StatusInternalServerError, "user deletion failed") return } // Remove all IMAP accounts that belonged to this user imapDeleted := 0 if s.imapStore != nil { if n, err := s.imapStore.DeleteByOwner(r.Context(), target.Username); err != nil { s.logger.Warn("delete user: could not remove IMAP accounts", "user", target.Username, "err", err) } else { imapDeleted = n } } s.audlog.Log(audit.Entry{ EventType: audit.EventUserMgmt, Username: sess.Username, TenantID: sess.TenantID, IPAddress: s.remoteIP(r), Detail: fmt.Sprintf( "deleted user %d (%s, role=%s); %d IMAP account(s) removed; emails retained per GoBD", id, target.Username, target.Role, imapDeleted, ), Success: true, }) writeJSON(w, http.StatusOK, map[string]bool{"ok": true}) }