You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@answer.apache.org by li...@apache.org on 2026/10/12 06:58:39 UTC
(answer) 02/02: fix(security): enforce profile content visibility
This is an automated email from the ASF dual-hosted git repository.
LinkinStars pushed a commit to branch fix/2.0.4/user
in repository https://gitbox.apache.org/repos/asf/answer.git
commit b45786f0a2c28c99d5851f62ef76ce75b2733bca
Author: LinkinStars <li...@foxmail.com>
AuthorDate: Sat Oct 10 13:58:01 2026 +0800
fix(security): enforce profile content visibility
Apply consistent public visibility rules when building profile content.
---
internal/entity/answer_entity.go | 11 ++--
internal/repo/answer/answer_repo.go | 21 +++++---
.../repo_test/personal_answer_visibility_test.go | 58 ++++++++++++++++++++++
internal/service/content/question_service.go | 10 +++-
4 files changed, 86 insertions(+), 14 deletions(-)
diff --git a/internal/entity/answer_entity.go b/internal/entity/answer_entity.go
index 0e93aa97..2c825391 100644
--- a/internal/entity/answer_entity.go
+++ b/internal/entity/answer_entity.go
@@ -57,11 +57,12 @@ type Answer struct {
type AnswerSearch struct {
Answer
- IncludeDeleted bool `json:"include_deleted"`
- LoginUserID string `json:"login_user_id"`
- Order string `json:"order_by"` // default or updated
- Page int `json:"page" form:"page"` // Query number of pages
- PageSize int `json:"page_size" form:"page_size"` // Search page size
+ IncludeDeleted bool `json:"include_deleted"`
+ OnlyPublicQuestion bool `json:"-"`
+ LoginUserID string `json:"login_user_id"`
+ Order string `json:"order_by"` // default or updated
+ Page int `json:"page" form:"page"` // Query number of pages
+ PageSize int `json:"page_size" form:"page_size"` // Search page size
}
type PersonalAnswerPageQueryCond struct {
diff --git a/internal/repo/answer/answer_repo.go b/internal/repo/answer/answer_repo.go
index 615d8dfb..40f78b3a 100644
--- a/internal/repo/answer/answer_repo.go
+++ b/internal/repo/answer/answer_repo.go
@@ -348,28 +348,33 @@ func (ar *answerRepo) SearchList(ctx context.Context, search *entity.AnswerSearc
}
offset := search.Page * search.PageSize
session := ar.data.DB.Context(ctx)
+ if search.OnlyPublicQuestion {
+ session = session.Join("INNER", "question", "answer.question_id = question.id").
+ And("question.show = ? AND question.status IN (?, ?)",
+ entity.QuestionShow, entity.QuestionStatusAvailable, entity.QuestionStatusClosed)
+ }
if search.QuestionID != "" {
- session = session.And("question_id = ?", search.QuestionID)
+ session = session.And("answer.question_id = ?", search.QuestionID)
}
if len(search.UserID) > 0 {
- session = session.And("user_id = ?", search.UserID)
+ session = session.And("answer.user_id = ?", search.UserID)
}
switch search.Order {
case entity.AnswerSearchOrderByTime:
- session = session.OrderBy("created_at desc")
+ session = session.OrderBy("answer.created_at desc")
case entity.AnswerSearchOrderByTimeAsc:
- session = session.OrderBy("created_at asc")
+ session = session.OrderBy("answer.created_at asc")
case entity.AnswerSearchOrderByVote:
- session = session.OrderBy("vote_count desc")
+ session = session.OrderBy("answer.vote_count desc")
default:
- session = session.OrderBy("adopted desc,vote_count desc,created_at asc")
+ session = session.OrderBy("answer.adopted desc,answer.vote_count desc,answer.created_at asc")
}
if !search.IncludeDeleted {
if search.LoginUserID == "" {
- session = session.And("status = ? ", entity.AnswerStatusAvailable)
+ session = session.And("answer.status = ? ", entity.AnswerStatusAvailable)
} else {
- session = session.And("status = ? OR user_id = ?", entity.AnswerStatusAvailable, search.LoginUserID)
+ session = session.And("answer.status = ? OR answer.user_id = ?", entity.AnswerStatusAvailable, search.LoginUserID)
}
}
diff --git a/internal/repo/repo_test/personal_answer_visibility_test.go b/internal/repo/repo_test/personal_answer_visibility_test.go
index f50105aa..254ddfd2 100644
--- a/internal/repo/repo_test/personal_answer_visibility_test.go
+++ b/internal/repo/repo_test/personal_answer_visibility_test.go
@@ -87,3 +87,61 @@ func TestPersonalAnswerPageRespectsHiddenQuestionVisibility(t *testing.T) {
require.Equal(t, int64(2), ownerTotal)
require.Len(t, ownerAnswers, 2)
}
+
+func TestAnswerSearchRespectsPublicParentQuestionVisibility(t *testing.T) {
+ ctx := context.Background()
+ questionRepo := question.NewQuestionRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource))
+ answerRepo := answer.NewAnswerRepo(testDataSource, nil, nil, nil)
+
+ newQuestion := func(title string, status, show int) *entity.Question {
+ q := &entity.Question{
+ UserID: "top-answer-question-owner",
+ Title: title,
+ OriginalText: title,
+ ParsedText: title,
+ Status: status,
+ Show: show,
+ }
+ require.NoError(t, questionRepo.AddQuestion(ctx, q))
+ return q
+ }
+ visibleQuestion := newQuestion("visible question", entity.QuestionStatusAvailable, entity.QuestionShow)
+ closedQuestion := newQuestion("closed question", entity.QuestionStatusClosed, entity.QuestionShow)
+ hiddenQuestion := newQuestion("hidden question", entity.QuestionStatusAvailable, entity.QuestionHide)
+ pendingQuestion := newQuestion("pending question", entity.QuestionStatusPending, entity.QuestionShow)
+ deletedQuestion := newQuestion("deleted question", entity.QuestionStatusDeleted, entity.QuestionShow)
+ questions := []*entity.Question{visibleQuestion, closedQuestion, hiddenQuestion, pendingQuestion, deletedQuestion}
+ answers := make([]*entity.Answer, 0, len(questions))
+ for _, q := range questions {
+ item := &entity.Answer{QuestionID: q.ID, UserID: "top-answer-author", OriginalText: "answer", ParsedText: "answer", Status: entity.AnswerStatusAvailable}
+ _, err := testDataSource.DB.Context(ctx).Insert(item)
+ require.NoError(t, err)
+ answers = append(answers, item)
+ }
+ t.Cleanup(func() {
+ for _, item := range answers {
+ _, _ = testDataSource.DB.Context(ctx).ID(item.ID).Delete(&entity.Answer{})
+ }
+ for _, q := range questions {
+ _, _ = testDataSource.DB.Context(ctx).ID(q.ID).Delete(&entity.Question{})
+ }
+ })
+
+ publicAnswers, total, err := answerRepo.SearchList(ctx, &entity.AnswerSearch{
+ Answer: entity.Answer{UserID: "top-answer-author"},
+ OnlyPublicQuestion: true,
+ PageSize: 10,
+ })
+ require.NoError(t, err)
+ require.Equal(t, int64(2), total)
+ require.Len(t, publicAnswers, 2)
+ questionIDs := map[string]bool{}
+ for _, item := range publicAnswers {
+ questionIDs[item.QuestionID] = true
+ }
+ require.True(t, questionIDs[visibleQuestion.ID])
+ require.True(t, questionIDs[closedQuestion.ID])
+ require.False(t, questionIDs[hiddenQuestion.ID])
+ require.False(t, questionIDs[pendingQuestion.ID])
+ require.False(t, questionIDs[deletedQuestion.ID])
+}
diff --git a/internal/service/content/question_service.go b/internal/service/content/question_service.go
index ed33f9ea..3350ef9b 100644
--- a/internal/service/content/question_service.go
+++ b/internal/service/content/question_service.go
@@ -1319,6 +1319,10 @@ func (qs *QuestionService) PersonalCollectionPage(ctx context.Context, req *sche
}
func canViewPersonalCollectionQuestion(question *schema.QuestionInfoResp, userID string, isAdminModerator bool) bool {
+ return canViewQuestion(question, userID, isAdminModerator)
+}
+
+func canViewQuestion(question *schema.QuestionInfoResp, userID string, isAdminModerator bool) bool {
return (&schema.SimpleObjectInfo{
ObjectType: constant.QuestionObjectType,
QuestionCreatorUserID: question.UserID,
@@ -1354,6 +1358,7 @@ func (qs *QuestionService) SearchUserTopList(ctx context.Context, userName strin
answersearch.UserID = userinfo.ID
answersearch.PageSize = 5
answersearch.Order = entity.AnswerSearchOrderByVote
+ answersearch.OnlyPublicQuestion = true
questionIDs := make([]string, 0)
answerList, _, err := qs.questioncommon.AnswerCommon.Search(ctx, answersearch)
if err != nil {
@@ -1370,7 +1375,7 @@ func (qs *QuestionService) SearchUserTopList(ctx context.Context, userName strin
}
for _, item := range answerlist {
_, ok := questionMaps[item.QuestionID]
- if ok {
+ if ok && canViewQuestion(questionMaps[item.QuestionID], "", false) {
item.QuestionInfo = questionMaps[item.QuestionID]
}
}
@@ -1383,6 +1388,9 @@ func (qs *QuestionService) SearchUserTopList(ctx context.Context, userName strin
}
for _, item := range answerlist {
+ if item.QuestionInfo == nil {
+ continue
+ }
info := &schema.UserAnswerInfo{}
_ = copier.Copy(info, item)
info.AnswerID = item.ID