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/08/28 06:31:31 UTC

(answer) 01/04: fix(security): enforce personal comment visibility

This is an automated email from the ASF dual-hosted git repository.

LinkinStars pushed a commit to branch fix/2.0.3/user
in repository https://gitbox.apache.org/repos/asf/answer.git

commit a52cc0aeb2b071ad73fc918c75a9b799b13352a2
Author: LinkinStars <li...@foxmail.com>
AuthorDate: Tue Aug 25 16:15:06 2026 +0800

    fix(security): enforce personal comment visibility
---
 internal/controller/comment_controller.go          |  6 +-
 internal/schema/comment_schema.go                  |  4 +-
 internal/service/comment/comment_service.go        | 44 ++++++----
 .../comment/comment_service_visibility_test.go     | 95 ++++++++++++++++++++++
 4 files changed, 129 insertions(+), 20 deletions(-)

diff --git a/internal/controller/comment_controller.go b/internal/controller/comment_controller.go
index b9beead9..e0f18a8a 100644
--- a/internal/controller/comment_controller.go
+++ b/internal/controller/comment_controller.go
@@ -280,7 +280,11 @@ func (cc *CommentController) GetCommentPersonalWithPage(ctx *gin.Context) {
 		return
 	}
 
-	req.UserID = middleware.GetLoginUserIDFromContext(ctx)
+	req.LoginUserID = middleware.GetLoginUserIDFromContext(ctx)
+	if len(req.Username) == 0 {
+		req.UserID = req.LoginUserID
+	}
+	req.IsAdminModerator = middleware.GetUserIsAdminModerator(ctx)
 
 	resp, err := cc.commentService.GetCommentPersonalWithPage(ctx, req)
 	handler.HandleResponse(ctx, err, resp)
diff --git a/internal/schema/comment_schema.go b/internal/schema/comment_schema.go
index a9c8a21a..8297c880 100644
--- a/internal/schema/comment_schema.go
+++ b/internal/schema/comment_schema.go
@@ -232,7 +232,9 @@ type GetCommentPersonalWithPageReq struct {
 	// username
 	Username string `validate:"omitempty,gt=0,lte=100" form:"username"`
 	// user id
-	UserID string `json:"-"`
+	UserID           string `json:"-"`
+	LoginUserID      string `json:"-"`
+	IsAdminModerator bool   `json:"-"`
 }
 
 // GetCommentPersonalWithPageResp comment response
diff --git a/internal/service/comment/comment_service.go b/internal/service/comment/comment_service.go
index 0decd584..455ab864 100644
--- a/internal/service/comment/comment_service.go
+++ b/internal/service/comment/comment_service.go
@@ -539,32 +539,40 @@ func (cs *CommentService) GetCommentPersonalWithPage(ctx context.Context, req *s
 	}
 	resp := make([]*schema.GetCommentPersonalWithPageResp, 0)
 	for _, comment := range commentList {
+		if len(comment.ObjectID) == 0 {
+			continue
+		}
+		objInfo, err := cs.objectInfoService.GetInfo(ctx, comment.ObjectID)
+		if err != nil {
+			log.Error(err)
+			continue
+		}
+		if !canViewPersonalComment(objInfo, req.LoginUserID, req.IsAdminModerator) {
+			continue
+		}
 		commentResp := &schema.GetCommentPersonalWithPageResp{
-			CommentID: comment.ID,
-			CreatedAt: comment.CreatedAt.Unix(),
-			ObjectID:  comment.ObjectID,
-			Content:   comment.ParsedText, // todo trim
+			CommentID:  comment.ID,
+			CreatedAt:  comment.CreatedAt.Unix(),
+			ObjectID:   comment.ObjectID,
+			Content:    comment.ParsedText, // todo trim
+			ObjectType: objInfo.ObjectType,
+			Title:      objInfo.Title,
+			UrlTitle:   htmltext.UrlTitle(objInfo.Title),
+			QuestionID: objInfo.QuestionID,
+			AnswerID:   objInfo.AnswerID,
 		}
-		if len(comment.ObjectID) > 0 {
-			objInfo, err := cs.objectInfoService.GetInfo(ctx, comment.ObjectID)
-			if err != nil {
-				log.Error(err)
-			} else {
-				commentResp.ObjectType = objInfo.ObjectType
-				commentResp.Title = objInfo.Title
-				commentResp.UrlTitle = htmltext.UrlTitle(objInfo.Title)
-				commentResp.QuestionID = objInfo.QuestionID
-				commentResp.AnswerID = objInfo.AnswerID
-				if objInfo.QuestionStatus == entity.QuestionStatusDeleted {
-					commentResp.Title = "Deleted question"
-				}
-			}
+		if objInfo.QuestionStatus == entity.QuestionStatusDeleted {
+			commentResp.Title = "Deleted question"
 		}
 		resp = append(resp, commentResp)
 	}
 	return pager.NewPageModel(total, resp), nil
 }
 
+func canViewPersonalComment(objInfo *schema.SimpleObjectInfo, userID string, isAdminModerator bool) bool {
+	return objInfo.CheckVisibility(userID, isAdminModerator) == nil
+}
+
 func (cs *CommentService) notificationQuestionComment(ctx context.Context, questionUserID,
 	questionID, questionTitle, commentID, commentUserID, commentSummary string) {
 	if questionUserID == commentUserID {
diff --git a/internal/service/comment/comment_service_visibility_test.go b/internal/service/comment/comment_service_visibility_test.go
new file mode 100644
index 00000000..69be2a96
--- /dev/null
+++ b/internal/service/comment/comment_service_visibility_test.go
@@ -0,0 +1,95 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package comment
+
+import (
+	"testing"
+
+	"github.com/apache/answer/internal/base/constant"
+	"github.com/apache/answer/internal/entity"
+	"github.com/apache/answer/internal/schema"
+)
+
+func TestCanViewPersonalComment(t *testing.T) {
+	testCases := []struct {
+		name     string
+		status   int
+		show     int
+		viewer   string
+		isAdmin  bool
+		expected bool
+	}{
+		{"anonymous can view public question", entity.QuestionStatusAvailable, entity.QuestionShow, "", false, true},
+		{"anonymous cannot view hidden question", entity.QuestionStatusAvailable, entity.QuestionHide, "", false, false},
+		{"anonymous cannot view pending question", entity.QuestionStatusPending, entity.QuestionShow, "", false, false},
+		{"anonymous cannot view deleted question", entity.QuestionStatusDeleted, entity.QuestionShow, "", false, false},
+		{"question owner can view hidden question", entity.QuestionStatusAvailable, entity.QuestionHide, "question-owner", false, true},
+		{"moderator can view deleted question", entity.QuestionStatusDeleted, entity.QuestionShow, "", true, true},
+	}
+
+	for _, testCase := range testCases {
+		t.Run(testCase.name, func(t *testing.T) {
+			objInfo := &schema.SimpleObjectInfo{
+				ObjectType:            constant.QuestionObjectType,
+				QuestionCreatorUserID: "question-owner",
+				QuestionStatus:        testCase.status,
+				QuestionShow:          testCase.show,
+			}
+			if got := canViewPersonalComment(objInfo, testCase.viewer, testCase.isAdmin); got != testCase.expected {
+				t.Fatalf("canViewPersonalComment() = %v, want %v", got, testCase.expected)
+			}
+		})
+	}
+}
+
+func TestCanViewPersonalCommentOnAnswer(t *testing.T) {
+	testCases := []struct {
+		name     string
+		viewer   string
+		isAdmin  bool
+		info     *schema.SimpleObjectInfo
+		expected bool
+	}{
+		{
+			name:     "anonymous cannot view answer on hidden question",
+			info:     &schema.SimpleObjectInfo{ObjectType: constant.AnswerObjectType, ObjectCreatorUserID: "answer-owner", QuestionID: "question-id", QuestionCreatorUserID: "question-owner", AnswerStatus: entity.AnswerStatusAvailable, QuestionStatus: entity.QuestionStatusAvailable, QuestionShow: entity.QuestionHide},
+			expected: false,
+		},
+		{
+			name:     "question owner can view answer on hidden question",
+			viewer:   "question-owner",
+			info:     &schema.SimpleObjectInfo{ObjectType: constant.AnswerObjectType, ObjectCreatorUserID: "answer-owner", QuestionID: "question-id", QuestionCreatorUserID: "question-owner", AnswerStatus: entity.AnswerStatusAvailable, QuestionStatus: entity.QuestionStatusAvailable, QuestionShow: entity.QuestionHide},
+			expected: true,
+		},
+		{
+			name:     "anonymous cannot view pending answer",
+			info:     &schema.SimpleObjectInfo{ObjectType: constant.AnswerObjectType, ObjectCreatorUserID: "answer-owner", QuestionID: "question-id", QuestionCreatorUserID: "question-owner", AnswerStatus: entity.AnswerStatusPending, QuestionStatus: entity.QuestionStatusAvailable, QuestionShow: entity.QuestionShow},
+			expected: false,
+		},
+	}
+
+	for _, testCase := range testCases {
+		t.Run(testCase.name, func(t *testing.T) {
+			if got := canViewPersonalComment(testCase.info, testCase.viewer, testCase.isAdmin); got != testCase.expected {
+				t.Fatalf("canViewPersonalComment() = %v, want %v", got, testCase.expected)
+			}
+		})
+	}
+}