feat(server): 支持上传文件关联会话话题并做权限校验

新增上传时读取会话话题参数,校验上传者是否为该话题的成员,通过后将文件与话题关联,允许其他成员下载。权限校验仅针对点对点和群组话题,关联失败不会阻止文件上传。
This commit is contained in:
Austin
2026-10-13 11:59:36 +08:00
parent 6be7796049
commit f3b1a06da5
+38 -1
View File
@@ -239,6 +239,9 @@ func largeFileReceiveHTTP(wrt http.ResponseWriter, req *http.Request) {
}
msgID := req.FormValue("id")
// When the uploader passes a conversation topic, the file is linked to it so other
// members of that conversation can download it (see TenantFileUserHasAccess).
uploadTopic := strings.TrimSpace(req.FormValue("topic"))
// Check authorization: either auth information or SID must be present
authMethod, secret := getHttpAuth(req)
principal, challenge, err := authFileRequest(authMethod, secret, req.FormValue("sid"), getRemoteAddr(req))
@@ -250,11 +253,17 @@ func largeFileReceiveHTTP(wrt http.ResponseWriter, req *http.Request) {
writeHttpResponse(InfoChallenge(msgID, now, challenge), nil)
return
}
if principal.UID.IsZero() && req.FormValue("topic") != "newacc" {
if principal.UID.IsZero() && uploadTopic != "newacc" {
// Not authenticated and not signup.
writeHttpResponse(ErrAuthRequired(msgID, "", now, now), nil)
return
}
if !principal.UID.IsZero() && uploadTopic != "" {
if err = authorizeUploadTopicLink(principal.TenantID, principal.UID, uploadTopic); err != nil {
writeHttpResponse(decodeStoreError(err, msgID, now, nil), err)
return
}
}
// Check if uploads are handled elsewhere.
headers, statusCode, err := mh.Headers(principal.TenantID, req.Method, req.URL, req.Header, true)
if err != nil {
@@ -361,6 +370,13 @@ func largeFileReceiveHTTP(wrt http.ResponseWriter, req *http.Request) {
logs.Warn.Printf("media upload: failed to enqueue file lifecycle event file=%s: %v", fdef.Id, err)
}
if uploadTopic != "" {
if err = store.Files.LinkAttachments(principal.TenantID, uploadTopic, types.ZeroUid, []string{fdef.Uid().String()}); err != nil {
// The file itself is valid; linking is best-effort so do not fail the upload.
logs.Warn.Printf("media upload: failed to link file %s to topic %s: %v", fdef.Id, uploadTopic, err)
}
}
params := map[string]any{"url": url, "file_id": fdef.Uid().String()}
if globals.mediaGcPeriod > 0 {
// How long this file is guaranteed to exist without being attached to a message or a topic.
@@ -484,6 +500,27 @@ func authorizeLargeFileRequestURL(tenantID types.TenantID, uid types.Uid, rawURL
return authorizeLargeFileDownload(tenantID, uid, rawURL, fd)
}
// authorizeUploadTopicLink verifies the uploader is a reading member of the conversation
// topic, so the uploaded file may be linked to it and downloaded by other members.
func authorizeUploadTopicLink(tenantID types.TenantID, uid types.Uid, topic string) error {
switch types.GetTopicCat(topic) {
case types.TopicCatP2P, types.TopicCatGrp:
default:
return types.ErrPermissionDenied
}
if store.Store == nil || !store.Store.IsOpen() {
return types.ErrUnsupported
}
sub, err := store.Subs.Get(tenantID, topic, uid, false)
if err != nil || sub == nil || sub.DeletedAt != nil {
return types.ErrPermissionDenied
}
if !sub.ModeWant.IsReader() || !sub.ModeGiven.IsReader() {
return types.ErrPermissionDenied
}
return nil
}
func authorizeLargeFileDownload(tenantID types.TenantID, uid types.Uid, rawURL string, fd *types.FileDef) error {
if fd == nil {
return types.ErrNotFound