fix: port mailbox validation from im server
This commit is contained in:
+23
-1
@@ -25,6 +25,11 @@ type API struct {
|
|||||||
|
|
||||||
const workbenchEventPublishTimeout = 15 * time.Second
|
const workbenchEventPublishTimeout = 15 * time.Second
|
||||||
|
|
||||||
|
const (
|
||||||
|
maxMailboxSubjectLength = 200
|
||||||
|
maxMailboxContentLength = 5000
|
||||||
|
)
|
||||||
|
|
||||||
func New(st *store.Store, authenticator *auth.Authenticator, publisher events.Publisher) *API {
|
func New(st *store.Store, authenticator *auth.Authenticator, publisher events.Publisher) *API {
|
||||||
if publisher == nil {
|
if publisher == nil {
|
||||||
publisher = events.NoopPublisher{}
|
publisher = events.NoopPublisher{}
|
||||||
@@ -879,9 +884,14 @@ func (a *API) mailboxSubmissions(w http.ResponseWriter, r *http.Request, p *auth
|
|||||||
if !decodeBody(w, r, &body) {
|
if !decodeBody(w, r, &body) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
subject := strings.TrimSpace(body.Subject)
|
||||||
|
content := strings.TrimSpace(body.Content)
|
||||||
|
if ok := validateMailboxSubmission(w, r, subject, content); !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
attachments, _ := json.Marshal(body.AttachmentFileIDs)
|
attachments, _ := json.Marshal(body.AttachmentFileIDs)
|
||||||
submission, err := a.store.CreateMailboxSubmission(r.Context(), p.UserID, store.MailboxSubmission{
|
submission, err := a.store.CreateMailboxSubmission(r.Context(), p.UserID, store.MailboxSubmission{
|
||||||
SubmissionID: "mb_" + randomToken(12), Subject: strings.TrimSpace(body.Subject), Content: strings.TrimSpace(body.Content),
|
SubmissionID: "mb_" + randomToken(12), Subject: subject, Content: content,
|
||||||
Anonymous: body.Anonymous, AttachmentFileIDs: string(attachments), NoticeVersion: strings.TrimSpace(body.NoticeVersion),
|
Anonymous: body.Anonymous, AttachmentFileIDs: string(attachments), NoticeVersion: strings.TrimSpace(body.NoticeVersion),
|
||||||
}, idempotencyKey)
|
}, idempotencyKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -896,6 +906,18 @@ func (a *API) mailboxSubmissions(w http.ResponseWriter, r *http.Request, p *auth
|
|||||||
writeSuccess(w, r, mailboxSubmissionPayload(*submission))
|
writeSuccess(w, r, mailboxSubmissionPayload(*submission))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func validateMailboxSubmission(w http.ResponseWriter, r *http.Request, subject, content string) bool {
|
||||||
|
if len([]rune(subject)) > maxMailboxSubjectLength {
|
||||||
|
writeError(w, r, http.StatusBadRequest, 30000, reasonValidationFailed, "validation failed", map[string]any{"field": "subject", "max_length": maxMailboxSubjectLength})
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if len([]rune(content)) > maxMailboxContentLength {
|
||||||
|
writeError(w, r, http.StatusBadRequest, 30000, reasonValidationFailed, "validation failed", map[string]any{"field": "content", "max_length": maxMailboxContentLength})
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
func (a *API) publishWorkbenchEvent(w http.ResponseWriter, r *http.Request, p *auth.Principal, module, action, resourceID string, targets []int64, title, summary string, data map[string]any) bool {
|
func (a *API) publishWorkbenchEvent(w http.ResponseWriter, r *http.Request, p *auth.Principal, module, action, resourceID string, targets []int64, title, summary string, data map[string]any) bool {
|
||||||
if a.events == nil {
|
if a.events == nil {
|
||||||
return true
|
return true
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package httpapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.118im.com/im_team/im_workbench/internal/store"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestValidateMailboxSubmissionRejectsTooLongContent(t *testing.T) {
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/v1/workbench/executive-mailbox/submissions", nil)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
|
||||||
|
if validateMailboxSubmission(rec, req, "建议", strings.Repeat("a", maxMailboxContentLength+1)) {
|
||||||
|
t.Fatal("validateMailboxSubmission returned true for too-long content")
|
||||||
|
}
|
||||||
|
if rec.Code != http.StatusBadRequest {
|
||||||
|
t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String())
|
||||||
|
}
|
||||||
|
var body apiError
|
||||||
|
if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil {
|
||||||
|
t.Fatalf("unmarshal response: %v", err)
|
||||||
|
}
|
||||||
|
if body.Reason != reasonValidationFailed {
|
||||||
|
t.Fatalf("reason=%q body=%s", body.Reason, rec.Body.String())
|
||||||
|
}
|
||||||
|
if body.Details["field"] != "content" {
|
||||||
|
t.Fatalf("details=%v", body.Details)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWriteStoreErrorMapsPolicyToValidationFailed(t *testing.T) {
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/v1/workbench/executive-mailbox/submissions", nil)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
|
||||||
|
writeStoreError(rec, req, store.ErrPolicy)
|
||||||
|
|
||||||
|
if rec.Code != http.StatusBadRequest {
|
||||||
|
t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String())
|
||||||
|
}
|
||||||
|
var body apiError
|
||||||
|
if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil {
|
||||||
|
t.Fatalf("unmarshal response: %v", err)
|
||||||
|
}
|
||||||
|
if body.Reason != reasonValidationFailed {
|
||||||
|
t.Fatalf("reason=%q body=%s", body.Reason, rec.Body.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,6 +17,7 @@ const (
|
|||||||
reasonForbidden = "FORBIDDEN"
|
reasonForbidden = "FORBIDDEN"
|
||||||
reasonNotFound = "RESOURCE_NOT_FOUND"
|
reasonNotFound = "RESOURCE_NOT_FOUND"
|
||||||
reasonConflict = "CONFLICT"
|
reasonConflict = "CONFLICT"
|
||||||
|
reasonValidationFailed = "VALIDATION_FAILED"
|
||||||
reasonInternal = "INTERNAL"
|
reasonInternal = "INTERNAL"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -57,6 +58,8 @@ func writeStoreError(w http.ResponseWriter, r *http.Request, err error) {
|
|||||||
writeError(w, r, http.StatusForbidden, 30041, reasonForbidden, "forbidden", nil)
|
writeError(w, r, http.StatusForbidden, 30041, reasonForbidden, "forbidden", nil)
|
||||||
case errors.Is(err, ErrConflict), errors.Is(err, store.ErrConflict):
|
case errors.Is(err, ErrConflict), errors.Is(err, store.ErrConflict):
|
||||||
writeError(w, r, http.StatusConflict, 30040, reasonConflict, "conflict", nil)
|
writeError(w, r, http.StatusConflict, 30040, reasonConflict, "conflict", nil)
|
||||||
|
case errors.Is(err, store.ErrPolicy):
|
||||||
|
writeError(w, r, http.StatusBadRequest, 30000, reasonValidationFailed, "validation failed", nil)
|
||||||
default:
|
default:
|
||||||
writeError(w, r, http.StatusInternalServerError, 90000, reasonInternal, "internal error", nil)
|
writeError(w, r, http.StatusInternalServerError, 90000, reasonInternal, "internal error", nil)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,4 +6,5 @@ var (
|
|||||||
ErrNotFound = errors.New("resource not found")
|
ErrNotFound = errors.New("resource not found")
|
||||||
ErrForbidden = errors.New("forbidden")
|
ErrForbidden = errors.New("forbidden")
|
||||||
ErrConflict = errors.New("conflict")
|
ErrConflict = errors.New("conflict")
|
||||||
|
ErrPolicy = errors.New("policy violation")
|
||||||
)
|
)
|
||||||
|
|||||||
+17
-1
@@ -719,8 +719,24 @@ func (s *Store) MailboxConfig(ctx context.Context) (*MailboxConfig, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) CreateMailboxSubmission(ctx context.Context, userID int64, submission MailboxSubmission, idempotencyKey string) (*MailboxSubmission, error) {
|
func (s *Store) CreateMailboxSubmission(ctx context.Context, userID int64, submission MailboxSubmission, idempotencyKey string) (*MailboxSubmission, error) {
|
||||||
|
cfg, err := s.MailboxConfig(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
submission.Subject = strings.TrimSpace(submission.Subject)
|
||||||
|
submission.Content = strings.TrimSpace(submission.Content)
|
||||||
|
if !cfg.Enabled || (submission.Anonymous && !cfg.AllowAnonymous) {
|
||||||
|
return nil, ErrPolicy
|
||||||
|
}
|
||||||
|
maxLength := cfg.MaxLength
|
||||||
|
if maxLength <= 0 {
|
||||||
|
maxLength = 5000
|
||||||
|
}
|
||||||
|
if len([]rune(submission.Subject)) > 200 || len([]rune(submission.Content)) > maxLength {
|
||||||
|
return nil, ErrPolicy
|
||||||
|
}
|
||||||
now := time.Now().UTC().Round(time.Millisecond)
|
now := time.Now().UTC().Round(time.Millisecond)
|
||||||
_, err := s.db.ExecContext(ctx, `INSERT INTO im_mailbox_submissions(submission_id,sender_id,subject,content,anonymous,attachment_file_ids,notice_version,idempotency_key,status,submitted_at)
|
_, err = s.db.ExecContext(ctx, `INSERT INTO im_mailbox_submissions(submission_id,sender_id,subject,content,anonymous,attachment_file_ids,notice_version,idempotency_key,status,submitted_at)
|
||||||
VALUES(?,?,?,?,?,?,?,?,?,?)`, submission.SubmissionID, userID, submission.Subject, submission.Content, submission.Anonymous, submission.AttachmentFileIDs, submission.NoticeVersion, idempotencyKey, "received", now)
|
VALUES(?,?,?,?,?,?,?,?,?,?)`, submission.SubmissionID, userID, submission.Subject, submission.Content, submission.Anonymous, submission.AttachmentFileIDs, submission.NoticeVersion, idempotencyKey, "received", now)
|
||||||
if isDuplicate(err) && idempotencyKey != "" {
|
if isDuplicate(err) && idempotencyKey != "" {
|
||||||
var existing MailboxSubmission
|
var existing MailboxSubmission
|
||||||
|
|||||||
Reference in New Issue
Block a user