Files
Enterprise-Knowledge-Base/internal/application/service/knowledgebase_search_shared.go
T
wizardchen 928f5abe3f feat(rbac): lift org membership from per-user to per-tenant (Plan 3, #1303)
Before this change, the `organization_members` table keyed on user_id —
joining an org as a Viewer in tenant T meant only that one user got
visibility on shared KBs/agents, while the rest of T (including its
Owner) saw nothing. The mismatch surfaced as confusing UX bugs:
"my colleague shared this with us, why can't I see it?"

This change replaces `organization_members` with
`organization_tenant_members`. Membership and Org-RBAC role now key on
(org_id, tenant_id); `representative_user_id` stays as a display-only
label of the user who first brought the tenant in.

Permission resolution gains a 3-D cap so tenant-RBAC stays honest under
cross-tenant sharing:

  effective = min(share.Permission, tenant_org_role, tenant_role_cap)

where tenant_role_cap pins tenant Viewers to OrgRoleViewer regardless
of the org-level grant. A Viewer in their own tenant cannot edit a
shared KB even if the org granted Editor — preserves the
"Viewer = read-only everywhere" promise.

Migration 000045 backfills `organization_tenant_members` from the old
table by collapsing each (org, tenant) group to its max role, then
renames the old table to `organization_members_pre_plan3` for safe
rollback. Sqlite mirror (Lite mode) is fresh-bootstrap: the init
schema replaces the old table directly.

Routes: member-management URLs change from `:user_id` to `:tenant_id`.
Org create / join / leave now require Admin+ in the caller's tenant —
joining an Org affects everyone in T, so the decision can't sit with
a Viewer.

Frontend: OrganizationSettingsModal + store update to address members
by tenant_id.
2026-05-18 17:28:58 +08:00

165 lines
5.4 KiB
Go

package service
import (
"context"
"github.com/Tencent/WeKnora/internal/logger"
"github.com/Tencent/WeKnora/internal/types"
)
// fetchKnowledgeData gets knowledge data in batch.
func (s *knowledgeBaseService) fetchKnowledgeData(ctx context.Context,
tenantID uint64,
knowledgeIDs []string,
) (map[string]*types.Knowledge, error) {
knowledges, err := s.kgRepo.GetKnowledgeBatch(ctx, tenantID, knowledgeIDs)
if err != nil {
logger.ErrorWithFields(ctx, err, map[string]interface{}{
"tenant_id": tenantID,
"knowledge_ids": knowledgeIDs,
})
return nil, err
}
knowledgeMap := make(map[string]*types.Knowledge, len(knowledges))
for _, knowledge := range knowledges {
knowledgeMap[knowledge.ID] = knowledge
}
return knowledgeMap, nil
}
// fetchKnowledgeDataWithShared gets knowledge data in batch, including knowledge
// from shared KBs the user has access to.
func (s *knowledgeBaseService) fetchKnowledgeDataWithShared(ctx context.Context,
tenantID uint64,
knowledgeIDs []string,
) (map[string]*types.Knowledge, error) {
knowledgeMap, err := s.fetchKnowledgeData(ctx, tenantID, knowledgeIDs)
if err != nil {
return nil, err
}
missingIDs := s.findMissingIDs(knowledgeIDs, func(id string) bool {
return knowledgeMap[id] != nil
})
if len(missingIDs) == 0 {
return knowledgeMap, nil
}
logger.Infof(ctx, "[fetchKnowledgeDataWithShared] %d knowledge IDs not found in current tenant, attempting shared KB lookup", len(missingIDs))
userID, ok := s.extractUserID(ctx)
if !ok {
logger.Warnf(ctx, "[fetchKnowledgeDataWithShared] userID not found or empty in context, skipping shared KB lookup")
return knowledgeMap, nil
}
logger.Infof(ctx, "[fetchKnowledgeDataWithShared] Looking up %d missing knowledge IDs with userID=%s", len(missingIDs), userID)
callerTenantRole := types.TenantRoleFromContext(ctx)
for _, id := range missingIDs {
k, err := s.kgRepo.GetKnowledgeByIDOnly(ctx, id)
if err != nil || k == nil || k.KnowledgeBaseID == "" {
logger.Debugf(ctx, "[fetchKnowledgeDataWithShared] Knowledge %s not found or has no KB", id)
continue
}
hasPermission, err := s.kbShareService.HasTenantKBPermission(ctx, k.KnowledgeBaseID, tenantID, callerTenantRole, types.OrgRoleViewer)
if err != nil {
logger.Debugf(ctx, "[fetchKnowledgeDataWithShared] Permission check error for KB %s: %v", k.KnowledgeBaseID, err)
continue
}
if !hasPermission {
logger.Debugf(ctx, "[fetchKnowledgeDataWithShared] No permission for KB %s", k.KnowledgeBaseID)
continue
}
logger.Debugf(ctx, "[fetchKnowledgeDataWithShared] Found shared knowledge %s in KB %s", id, k.KnowledgeBaseID)
knowledgeMap[k.ID] = k
}
logger.Infof(ctx, "[fetchKnowledgeDataWithShared] After shared lookup, total knowledge found: %d", len(knowledgeMap))
return knowledgeMap, nil
}
// listChunksByIDWithShared fetches chunks by IDs, including chunks from shared KBs the user has access to.
func (s *knowledgeBaseService) listChunksByIDWithShared(ctx context.Context,
tenantID uint64,
chunkIDs []string,
) ([]*types.Chunk, error) {
chunks, err := s.chunkRepo.ListChunksByID(ctx, tenantID, chunkIDs)
if err != nil {
return nil, err
}
foundSet := make(map[string]bool, len(chunks))
for _, c := range chunks {
if c != nil {
foundSet[c.ID] = true
}
}
missing := s.findMissingIDs(chunkIDs, func(id string) bool {
return foundSet[id]
})
if len(missing) == 0 {
return chunks, nil
}
logger.Infof(ctx, "[listChunksByIDWithShared] %d chunks not found in current tenant, attempting shared KB lookup", len(missing))
userID, ok := s.extractUserID(ctx)
if !ok {
logger.Warnf(ctx, "[listChunksByIDWithShared] userID not found or empty in context, skipping shared KB lookup")
return chunks, nil
}
logger.Infof(ctx, "[listChunksByIDWithShared] Looking up %d missing chunks with userID=%s", len(missing), userID)
callerTenantRole := types.TenantRoleFromContext(ctx)
crossChunks, err := s.chunkRepo.ListChunksByIDOnly(ctx, missing)
if err != nil {
logger.Warnf(ctx, "[listChunksByIDWithShared] Failed to fetch chunks by ID only: %v", err)
return chunks, nil
}
logger.Infof(ctx, "[listChunksByIDWithShared] Found %d chunks without tenant filter", len(crossChunks))
for _, c := range crossChunks {
if c == nil || c.KnowledgeBaseID == "" {
continue
}
hasPermission, err := s.kbShareService.HasTenantKBPermission(ctx, c.KnowledgeBaseID, tenantID, callerTenantRole, types.OrgRoleViewer)
if err != nil {
logger.Debugf(ctx, "[listChunksByIDWithShared] Permission check error for KB %s: %v", c.KnowledgeBaseID, err)
continue
}
if !hasPermission {
logger.Debugf(ctx, "[listChunksByIDWithShared] No permission for KB %s", c.KnowledgeBaseID)
continue
}
chunks = append(chunks, c)
}
logger.Infof(ctx, "[listChunksByIDWithShared] After shared lookup, total chunks: %d", len(chunks))
return chunks, nil
}
// findMissingIDs returns IDs from the input slice that are not found by the exists predicate.
func (s *knowledgeBaseService) findMissingIDs(ids []string, exists func(string) bool) []string {
var missing []string
for _, id := range ids {
if !exists(id) {
missing = append(missing, id)
}
}
return missing
}
// extractUserID extracts the user ID from context, returning ("", false) if not found.
func (s *knowledgeBaseService) extractUserID(ctx context.Context) (string, bool) {
userIDVal := ctx.Value(types.UserIDContextKey)
if userIDVal == nil {
return "", false
}
userID, ok := userIDVal.(string)
if !ok || userID == "" {
return "", false
}
return userID, true
}