fix(go-document): add document and file access checks (#16592)

## Summary
Adds ownership/access checks before updating or deleting documents,
setting document metadata, and reading file contents from storage. Also
adds tests for authorized and unauthorized access paths.
This commit is contained in:
Hz_
2026-07-06 10:13:46 +08:00
committed by GitHub
parent e5d217993b
commit 358152f758
9 changed files with 541 additions and 25 deletions

View File

@@ -330,7 +330,7 @@ func (h *DocumentHandler) GetDocumentPreview(c *gin.Context) {
// @Success 200 {object} map[string]interface{}
// @Router /api/v1/documents/{id} [put]
func (h *DocumentHandler) UpdateDocument(c *gin.Context) {
_, errorCode, errorMessage := GetUser(c)
user, errorCode, errorMessage := GetUser(c)
if errorCode != common.CodeSuccess {
jsonError(c, errorCode, errorMessage)
return
@@ -344,6 +344,19 @@ func (h *DocumentHandler) UpdateDocument(c *gin.Context) {
return
}
doc, err := h.documentService.GetDocumentByID(id)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"code": 1,
"message": "document not found",
})
return
}
if !h.datasetService.Accessible(doc.KbID, user.ID) {
jsonError(c, common.CodeAuthenticationError, "No authorization.")
return
}
var req service.UpdateDocumentRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
@@ -374,7 +387,7 @@ func (h *DocumentHandler) UpdateDocument(c *gin.Context) {
// @Success 200 {object} map[string]interface{}
// @Router /api/v1/documents/{id} [delete]
func (h *DocumentHandler) DeleteDocument(c *gin.Context) {
_, errorCode, errorMessage := GetUser(c)
user, errorCode, errorMessage := GetUser(c)
if errorCode != common.CodeSuccess {
jsonError(c, errorCode, errorMessage)
return
@@ -388,6 +401,19 @@ func (h *DocumentHandler) DeleteDocument(c *gin.Context) {
return
}
doc, err := h.documentService.GetDocumentByID(id)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"code": 1,
"message": "document not found",
})
return
}
if !h.datasetService.Accessible(doc.KbID, user.ID) {
jsonError(c, common.CodeAuthenticationError, "No authorization.")
return
}
if err := h.documentService.DeleteDocument(id); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": err.Error(),
@@ -1254,7 +1280,7 @@ type SetMetaRequest struct {
// @Success 200 {object} map[string]interface{}
// @Router /v1/document/set_meta [post]
func (h *DocumentHandler) SetMeta(c *gin.Context) {
_, errorCode, errorMessage := GetUser(c)
user, errorCode, errorMessage := GetUser(c)
if errorCode != common.CodeSuccess {
jsonError(c, errorCode, errorMessage)
return
@@ -1321,7 +1347,21 @@ func (h *DocumentHandler) SetMeta(c *gin.Context) {
}
}
err := h.documentService.SetDocumentMetadata(req.DocID, meta)
// Authorization: user must be able to access the document's dataset.
doc, err := h.documentService.GetDocumentByID(req.DocID)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"code": 1,
"message": "document not found",
})
return
}
if !h.datasetService.Accessible(doc.KbID, user.ID) {
jsonError(c, common.CodeAuthenticationError, "No authorization.")
return
}
err = h.documentService.SetDocumentMetadata(req.DocID, meta)
if err != nil {
errMsg := err.Error()
if strings.Contains(errMsg, "no such document") || strings.Contains(errMsg, "document not found") {

View File

@@ -40,6 +40,12 @@ import (
type fakeDocumentService struct {
deleted int
err error
doc *service.DocumentResponse
docErr error
updateCalled bool
updatedID string
deleteCalled bool
deletedID string
stopResult map[string]interface{}
stopErr error
thumbnails map[string]string
@@ -50,6 +56,9 @@ type fakeDocumentService struct {
metadataErr error
metadataKBID string
metadataDocIDs []string
setMetaCalled bool
setMetaDocID string
setMetaValue map[string]interface{}
uploadLocalData []map[string]interface{}
uploadLocalErrs []string
uploadLocalKB *entity.Knowledgebase
@@ -129,12 +138,22 @@ func (f *fakeDocumentService) CreateDocument(req *service.CreateDocumentRequest)
return nil, nil
}
func (f *fakeDocumentService) GetDocumentByID(id string) (*service.DocumentResponse, error) {
return nil, nil
if f.docErr != nil {
return nil, f.docErr
}
if f.doc != nil {
return f.doc, nil
}
return nil, fmt.Errorf("document not found")
}
func (f *fakeDocumentService) UpdateDocument(id string, req *service.UpdateDocumentRequest) error {
f.updateCalled = true
f.updatedID = id
return nil
}
func (f *fakeDocumentService) DeleteDocument(id string) error {
f.deleteCalled = true
f.deletedID = id
return nil
}
func (f *fakeDocumentService) DeleteDocuments(ids []string, deleteAll bool, datasetID, userID string) (int, error) {
@@ -193,6 +212,9 @@ func (f *fakeDocumentService) GetMetadataSummary(kbID string, docIDs []string) (
return f.metadataSummary, f.metadataErr
}
func (f *fakeDocumentService) SetDocumentMetadata(docID string, meta map[string]interface{}) error {
f.setMetaCalled = true
f.setMetaDocID = docID
f.setMetaValue = meta
return nil
}
func (f *fakeDocumentService) DeleteDocumentMetadata(docID string, keys []string) error {
@@ -242,6 +264,248 @@ func setupGinContextWithUser(method, path, body string) (*gin.Context, *httptest
return c, w
}
func setupDocumentPermissionDB(t *testing.T, accessible bool) {
t.Helper()
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{
TranslateError: true,
})
if err != nil {
t.Fatalf("failed to open sqlite: %v", err)
}
if err := db.AutoMigrate(
&entity.Knowledgebase{},
&entity.UserTenant{},
); err != nil {
t.Fatalf("failed to migrate permission tables: %v", err)
}
if err := db.Create(&entity.Knowledgebase{
ID: "kb-owner",
TenantID: "tenant-owner",
Name: "owner-kb",
EmbdID: "embd-1",
CreatedBy: "owner-user",
Permission: string(entity.TenantPermissionTeam),
Status: sptr(string(entity.StatusValid)),
}).Error; err != nil {
t.Fatalf("insert knowledgebase: %v", err)
}
if accessible {
if err := db.Create(&entity.UserTenant{
ID: "ut-user-1",
UserID: "user-1",
TenantID: "tenant-owner",
Role: "normal",
Status: sptr(string(entity.StatusValid)),
}).Error; err != nil {
t.Fatalf("insert user_tenant: %v", err)
}
}
orig := dao.DB
dao.DB = db
t.Cleanup(func() { dao.DB = orig })
}
func TestSetMetaHandler_NotAccessible(t *testing.T) {
setupDocumentPermissionDB(t, false)
fake := &fakeDocumentService{
doc: &service.DocumentResponse{ID: "doc-1", KbID: "kb-owner"},
}
h := &DocumentHandler{
documentService: fake,
datasetService: service.NewDatasetService(),
}
c, w := setupGinContextWithUser("POST", "/api/v1/document/set_meta", `{"doc_id":"doc-1","meta":"{\"poc\":\"blocked\"}"}`)
h.SetMeta(c)
if w.Code != http.StatusOK {
t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
}
var resp map[string]interface{}
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
t.Fatal(err)
}
if resp["code"] != float64(common.CodeAuthenticationError) {
t.Fatalf("expected auth error, got %v", resp)
}
if resp["message"] != "No authorization." {
t.Fatalf("unexpected message: %v", resp["message"])
}
if fake.setMetaCalled {
t.Fatal("SetDocumentMetadata should not be called without dataset access")
}
}
func TestSetMetaHandler_Accessible(t *testing.T) {
setupDocumentPermissionDB(t, true)
fake := &fakeDocumentService{
doc: &service.DocumentResponse{ID: "doc-1", KbID: "kb-owner"},
}
h := &DocumentHandler{
documentService: fake,
datasetService: service.NewDatasetService(),
}
c, w := setupGinContextWithUser("POST", "/api/v1/document/set_meta", `{"doc_id":"doc-1","meta":"{\"category\":\"tech\",\"year\":2026}"}`)
h.SetMeta(c)
if w.Code != http.StatusOK {
t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
}
var resp map[string]interface{}
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
t.Fatal(err)
}
if resp["code"] != float64(common.CodeSuccess) || resp["data"] != true {
t.Fatalf("unexpected response: %v", resp)
}
if !fake.setMetaCalled {
t.Fatal("SetDocumentMetadata should be called with dataset access")
}
if fake.setMetaDocID != "doc-1" {
t.Fatalf("set meta doc id = %q, want doc-1", fake.setMetaDocID)
}
if fake.setMetaValue["category"] != "tech" || fake.setMetaValue["year"] != float64(2026) {
t.Fatalf("unexpected meta: %#v", fake.setMetaValue)
}
}
func TestDeleteDocumentHandler_NotAccessible(t *testing.T) {
setupDocumentPermissionDB(t, false)
fake := &fakeDocumentService{
doc: &service.DocumentResponse{ID: "doc-1", KbID: "kb-owner"},
}
h := &DocumentHandler{
documentService: fake,
datasetService: service.NewDatasetService(),
}
c, w := setupGinContextWithUser("DELETE", "/api/v1/documents/doc-1", "")
c.Params = gin.Params{{Key: "id", Value: "doc-1"}}
h.DeleteDocument(c)
if w.Code != http.StatusOK {
t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
}
var resp map[string]interface{}
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
t.Fatal(err)
}
if resp["code"] != float64(common.CodeAuthenticationError) {
t.Fatalf("expected auth error, got %v", resp)
}
if resp["message"] != "No authorization." {
t.Fatalf("unexpected message: %v", resp["message"])
}
if fake.deleteCalled {
t.Fatal("DeleteDocument should not be called without dataset access")
}
}
func TestDeleteDocumentHandler_Accessible(t *testing.T) {
setupDocumentPermissionDB(t, true)
fake := &fakeDocumentService{
doc: &service.DocumentResponse{ID: "doc-1", KbID: "kb-owner"},
}
h := &DocumentHandler{
documentService: fake,
datasetService: service.NewDatasetService(),
}
c, w := setupGinContextWithUser("DELETE", "/api/v1/documents/doc-1", "")
c.Params = gin.Params{{Key: "id", Value: "doc-1"}}
h.DeleteDocument(c)
if w.Code != http.StatusOK {
t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
}
if !fake.deleteCalled {
t.Fatal("DeleteDocument should be called with dataset access")
}
if fake.deletedID != "doc-1" {
t.Fatalf("deleted id = %q, want doc-1", fake.deletedID)
}
var resp map[string]interface{}
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
t.Fatal(err)
}
if resp["message"] != "deleted successfully" {
t.Fatalf("unexpected response: %v", resp)
}
}
func TestUpdateDocumentHandler_NotAccessible(t *testing.T) {
setupDocumentPermissionDB(t, false)
fake := &fakeDocumentService{
doc: &service.DocumentResponse{ID: "doc-1", KbID: "kb-owner"},
}
h := &DocumentHandler{
documentService: fake,
datasetService: service.NewDatasetService(),
}
c, w := setupGinContextWithUser("PUT", "/api/v1/documents/doc-1", `{"name":"blocked"}`)
c.Params = gin.Params{{Key: "id", Value: "doc-1"}}
h.UpdateDocument(c)
if w.Code != http.StatusOK {
t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
}
var resp map[string]interface{}
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
t.Fatal(err)
}
if resp["code"] != float64(common.CodeAuthenticationError) {
t.Fatalf("expected auth error, got %v", resp)
}
if resp["message"] != "No authorization." {
t.Fatalf("unexpected message: %v", resp["message"])
}
if fake.updateCalled {
t.Fatal("UpdateDocument should not be called without dataset access")
}
}
func TestUpdateDocumentHandler_Accessible(t *testing.T) {
setupDocumentPermissionDB(t, true)
fake := &fakeDocumentService{
doc: &service.DocumentResponse{ID: "doc-1", KbID: "kb-owner"},
}
h := &DocumentHandler{
documentService: fake,
datasetService: service.NewDatasetService(),
}
c, w := setupGinContextWithUser("PUT", "/api/v1/documents/doc-1", `{"name":"allowed"}`)
c.Params = gin.Params{{Key: "id", Value: "doc-1"}}
h.UpdateDocument(c)
if w.Code != http.StatusOK {
t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
}
if !fake.updateCalled {
t.Fatal("UpdateDocument should be called with dataset access")
}
if fake.updatedID != "doc-1" {
t.Fatalf("updated id = %q, want doc-1", fake.updatedID)
}
var resp map[string]interface{}
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
t.Fatal(err)
}
if resp["message"] != "updated successfully" {
t.Fatalf("unexpected response: %v", resp)
}
}
func setupUploadHandlerDB(t *testing.T, role string) *gorm.DB {
t.Helper()