WeKnora deployment source with the reviewed wiki identity, summary and recovery fixes
Anydoc / anydoc parser engine (push) Canceled after 0s
App / Format, vet, test, and build (push) Canceled after 0s
CLI / test (macos-latest / go 1.26) (push) Canceled after 0s
CLI / test (ubuntu-latest / go 1.26) (push) Canceled after 0s
CLI / test (windows-latest / go 1.26) (push) Canceled after 0s
Build and Push Docker Image / build-ui (push) Canceled after 0s
Build and Push Docker Image / build-docreader (push) Canceled after 0s
Build and Push Docker Image / build-sandbox (push) Canceled after 0s
Build and Push Docker Image / build-app (amd64, linux/amd64, ubuntu-latest) (push) Canceled after 0s
Build and Push Docker Image / build-app (arm64, linux/arm64, ubuntu-24.04-arm) (push) Canceled after 0s
DocReader / Python tests and Go client tests (push) Canceled after 0s
Build & Publish (dsh-weknora) / Typecheck, test, pack (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (pinned) (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (latest, advisory) (push) Canceled after 0s
Frontend / Test, type-check, and build (push) Canceled after 0s
Go Lint Cache / warm lint cache (.) (push) Canceled after 0s
Go Lint Cache / warm lint cache (cli) (push) Canceled after 0s
Go Lint Cache / warm lint cache (client) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.10) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.11) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.12) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.13) (push) Canceled after 0s
Build and Push Docker Image / merge (push) Canceled after 0s
Build & Publish (dsh-weknora) / Publish to npm (push) Canceled after 0s
Build & Publish (mcp-server) / Build sdist + wheel (push) Canceled after 0s
Build & Publish (mcp-server) / Publish to PyPI (push) Canceled after 0s
Anydoc / anydoc parser engine (push) Canceled after 0s
App / Format, vet, test, and build (push) Canceled after 0s
CLI / test (macos-latest / go 1.26) (push) Canceled after 0s
CLI / test (ubuntu-latest / go 1.26) (push) Canceled after 0s
CLI / test (windows-latest / go 1.26) (push) Canceled after 0s
Build and Push Docker Image / build-ui (push) Canceled after 0s
Build and Push Docker Image / build-docreader (push) Canceled after 0s
Build and Push Docker Image / build-sandbox (push) Canceled after 0s
Build and Push Docker Image / build-app (amd64, linux/amd64, ubuntu-latest) (push) Canceled after 0s
Build and Push Docker Image / build-app (arm64, linux/arm64, ubuntu-24.04-arm) (push) Canceled after 0s
DocReader / Python tests and Go client tests (push) Canceled after 0s
Build & Publish (dsh-weknora) / Typecheck, test, pack (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (pinned) (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (latest, advisory) (push) Canceled after 0s
Frontend / Test, type-check, and build (push) Canceled after 0s
Go Lint Cache / warm lint cache (.) (push) Canceled after 0s
Go Lint Cache / warm lint cache (cli) (push) Canceled after 0s
Go Lint Cache / warm lint cache (client) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.10) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.11) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.12) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.13) (push) Canceled after 0s
Build and Push Docker Image / merge (push) Canceled after 0s
Build & Publish (dsh-weknora) / Publish to npm (push) Canceled after 0s
Build & Publish (mcp-server) / Build sdist + wheel (push) Canceled after 0s
Build & Publish (mcp-server) / Publish to PyPI (push) Canceled after 0s
This commit is contained in:
@@ -0,0 +1,223 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/Tencent/WeKnora/internal/accesspolicy"
|
||||
"github.com/Tencent/WeKnora/internal/types"
|
||||
"github.com/Tencent/WeKnora/internal/types/interfaces"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var errFileAccessDenied = errors.New("file is not attached to an accessible resource")
|
||||
|
||||
// A storage locator is never an authorization capability. Resolve it back to
|
||||
// live domain owners, then apply the same personal/knowledge scope as their APIs.
|
||||
func (r *resourceRepository) AuthorizeFileRead(ctx context.Context, value string, scope interfaces.ResourceFileScope) error {
|
||||
if scope.TenantID == 0 {
|
||||
return errFileAccessDenied
|
||||
}
|
||||
var resource *types.StoredResource
|
||||
var err error
|
||||
if handle, ok := types.ParseResourcePath(value); ok {
|
||||
resource, err = r.GetByHandle(ctx, handle)
|
||||
} else {
|
||||
hash := sha256.Sum256([]byte(value))
|
||||
resource, err = r.GetByTenantLocation(ctx, scope.TenantID, hex.EncodeToString(hash[:]))
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var bindings []types.ResourceBinding
|
||||
if resource != nil {
|
||||
if resource.TenantID != scope.TenantID {
|
||||
return errFileAccessDenied
|
||||
}
|
||||
if err := r.db.WithContext(ctx).Where("resource_id = ? AND tenant_id = ?", resource.ID, scope.TenantID).Find(&bindings).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
// Older registrations may lack a source_file relation. An attachment
|
||||
// alias for the same original must not bypass the download restriction.
|
||||
var originals []types.Knowledge
|
||||
if err := r.db.WithContext(ctx).Select("id").Where("tenant_id = ? AND file_path IN ?", scope.TenantID,
|
||||
[]string{value, resource.PhysicalPath, types.BuildResourcePath(resource.Handle)}).Find(&originals).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
for _, doc := range originals {
|
||||
bindings = append(bindings, types.ResourceBinding{OwnerType: types.ResourceOwnerKnowledge, OwnerID: doc.ID, Relation: types.ResourceRelationSourceFile})
|
||||
}
|
||||
} else if _, isHandle := types.ParseResourcePath(value); !isHandle {
|
||||
// Legacy source files predate resource bindings but still have an exact
|
||||
// knowledge owner. Do not accept an arbitrary same-tenant path.
|
||||
var documents []types.Knowledge
|
||||
if err := r.db.WithContext(ctx).Select("id").Where("tenant_id = ? AND file_path = ?", scope.TenantID, value).Find(&documents).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
for _, doc := range documents {
|
||||
bindings = append(bindings, types.ResourceBinding{OwnerType: types.ResourceOwnerKnowledge, OwnerID: doc.ID, Relation: types.ResourceRelationSourceFile})
|
||||
}
|
||||
}
|
||||
var sourceBindings []types.ResourceBinding
|
||||
for _, binding := range bindings {
|
||||
if binding.OwnerType == types.ResourceOwnerKnowledge && binding.Relation == types.ResourceRelationSourceFile {
|
||||
sourceBindings = append(sourceBindings, binding)
|
||||
}
|
||||
}
|
||||
if len(sourceBindings) > 0 {
|
||||
bindings = sourceBindings
|
||||
}
|
||||
for _, binding := range bindings {
|
||||
allowed, err := r.readableFileBinding(ctx, scope, binding)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if allowed {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return errFileAccessDenied
|
||||
}
|
||||
|
||||
func (r *resourceRepository) readableFileBinding(ctx context.Context, scope interfaces.ResourceFileScope, binding types.ResourceBinding) (bool, error) {
|
||||
q := r.db.WithContext(ctx)
|
||||
callerTenant, _ := types.TenantIDFromContext(ctx)
|
||||
userID, _ := types.UserIDFromContext(ctx)
|
||||
switch binding.OwnerType {
|
||||
case types.ResourceOwnerKnowledge:
|
||||
if binding.Relation == types.ResourceRelationSourceFile {
|
||||
if callerTenant != scope.TenantID || userID == "" || scope.EmbedChannelID != "" || !accesspolicy.Allowed(ctx, "knowledge.read") {
|
||||
return false, nil
|
||||
}
|
||||
var source types.Knowledge
|
||||
if err := q.Select("id,knowledge_base_id").Where("id = ? AND tenant_id = ?", binding.OwnerID, scope.TenantID).Take(&source).Error; err != nil {
|
||||
return false, ignoreMissingFileOwner(err)
|
||||
}
|
||||
var base types.KnowledgeBase
|
||||
if err := q.Select("id,creator_id").Where("id = ? AND tenant_id = ?", source.KnowledgeBaseID, scope.TenantID).Take(&base).Error; err != nil {
|
||||
return false, ignoreMissingFileOwner(err)
|
||||
}
|
||||
if base.CreatorID != userID && !types.IsSystemAdminFromContext(ctx) && !types.TenantRoleFromContext(ctx).HasPermission(types.TenantRoleAdmin) {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
if scope.MessageID != "" {
|
||||
var message types.Message
|
||||
if err := q.Select("id,session_id,knowledge_references").Where("id = ?", scope.MessageID).Take(&message).Error; err != nil {
|
||||
return false, ignoreMissingFileOwner(err)
|
||||
}
|
||||
for _, reference := range message.KnowledgeReferences {
|
||||
if reference != nil && reference.KnowledgeID == binding.OwnerID {
|
||||
var count int64
|
||||
if err := q.Model(&types.Knowledge{}).Where("id = ? AND tenant_id = ?", binding.OwnerID, scope.TenantID).Count(&count).Error; err != nil {
|
||||
return false, err
|
||||
}
|
||||
if count == 0 {
|
||||
return false, nil
|
||||
}
|
||||
return r.readableFileSession(ctx, scope, message.SessionID)
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
var knowledge types.Knowledge
|
||||
if err := q.Select("id,knowledge_base_id").Where("id = ? AND tenant_id = ?", binding.OwnerID, scope.TenantID).Take(&knowledge).Error; err != nil {
|
||||
return false, ignoreMissingFileOwner(err)
|
||||
}
|
||||
if scope.KnowledgeBaseID != "" {
|
||||
// KBAccessRead has already authorized this exact knowledge base.
|
||||
return knowledge.KnowledgeBaseID == scope.KnowledgeBaseID, nil
|
||||
}
|
||||
var base types.KnowledgeBase
|
||||
if err := q.Select("id,tenant_id,creator_id,is_temporary,version_state").Where("id = ? AND tenant_id = ?", knowledge.KnowledgeBaseID, scope.TenantID).Take(&base).Error; err != nil {
|
||||
return false, ignoreMissingFileOwner(err)
|
||||
}
|
||||
if base.IsTemporary || base.VersionState != "" {
|
||||
return false, nil
|
||||
}
|
||||
if scope.EmbedChannelID != "" {
|
||||
var channel types.EmbedChannel
|
||||
if err := q.Where("id = ? AND tenant_id = ? AND enabled = ?", scope.EmbedChannelID, scope.TenantID, true).Take(&channel).Error; err != nil {
|
||||
return false, ignoreMissingFileOwner(err)
|
||||
}
|
||||
var agent types.CustomAgent
|
||||
if err := q.Where("id = ? AND tenant_id = ? AND enabled = ?", channel.AgentID, scope.TenantID, true).Take(&agent).Error; err != nil {
|
||||
return false, ignoreMissingFileOwner(err)
|
||||
}
|
||||
switch agent.Config.KBSelectionMode {
|
||||
case "none":
|
||||
return false, nil
|
||||
case "selected":
|
||||
for _, id := range agent.Config.KnowledgeBases {
|
||||
if id == base.ID {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
default:
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
if callerTenant != scope.TenantID || userID == "" || !accesspolicy.Allowed(ctx, "knowledge.read") {
|
||||
return false, nil
|
||||
}
|
||||
if types.IsSystemAdminFromContext(ctx) || types.TenantRoleFromContext(ctx).HasPermission(types.TenantRoleAdmin) {
|
||||
return true, nil
|
||||
}
|
||||
return accesspolicy.Readable(ctx, "knowledge", base.ID, base.CreatorID == userID, accesspolicy.FromContext(ctx) == nil), nil
|
||||
case types.ResourceOwnerMessage, types.ResourceOwnerTemporaryDocument:
|
||||
userID = types.SessionOwnerIDFromContext(ctx)
|
||||
if scope.KnowledgeBaseID != "" || userID == "" {
|
||||
return false, nil
|
||||
}
|
||||
var sessionID string
|
||||
if binding.OwnerType == types.ResourceOwnerMessage {
|
||||
if scope.MessageID != "" && binding.OwnerID != scope.MessageID {
|
||||
return false, nil
|
||||
}
|
||||
var message types.Message
|
||||
if err := q.Select("id,session_id").Where("id = ?", binding.OwnerID).Take(&message).Error; err != nil {
|
||||
return false, ignoreMissingFileOwner(err)
|
||||
}
|
||||
sessionID = message.SessionID
|
||||
} else {
|
||||
if scope.MessageID != "" {
|
||||
return false, nil
|
||||
}
|
||||
var document types.TemporaryDocument
|
||||
if err := q.Select("session_id").Where("id = ? AND tenant_id = ? AND expires_at > ?", binding.OwnerID, scope.TenantID, time.Now()).Take(&document).Error; err != nil {
|
||||
return false, ignoreMissingFileOwner(err)
|
||||
}
|
||||
sessionID = document.SessionID
|
||||
}
|
||||
return r.readableFileSession(ctx, scope, sessionID)
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (r *resourceRepository) readableFileSession(ctx context.Context, scope interfaces.ResourceFileScope, sessionID string) (bool, error) {
|
||||
tenantID, _ := types.TenantIDFromContext(ctx)
|
||||
userID := types.SessionOwnerIDFromContext(ctx)
|
||||
if userID == "" || scope.SessionID != "" && scope.SessionID != sessionID {
|
||||
return false, nil
|
||||
}
|
||||
var session types.Session
|
||||
if err := r.db.WithContext(ctx).Select("id,tenant_id,user_id").Where("id = ? AND tenant_id = ? AND user_id = ?", sessionID, tenantID, userID).Take(&session).Error; err != nil {
|
||||
return false, ignoreMissingFileOwner(err)
|
||||
}
|
||||
if scope.EmbedChannelID != "" {
|
||||
return scope.SessionID != "" && strings.HasPrefix(userID, types.PrincipalEmbedSession+":"), nil
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func ignoreMissingFileOwner(err error) error {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
Reference in New Issue
Block a user