mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-07-19 22:21:04 +08:00
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:
@@ -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") {
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user