diff --git a/internal/im/identity_test.go b/internal/im/identity_test.go new file mode 100644 index 00000000..26b49040 --- /dev/null +++ b/internal/im/identity_test.go @@ -0,0 +1,38 @@ +package im + +import ( + "context" + "testing" + + "github.com/Tencent/WeKnora/internal/types" +) + +func TestWithIMIdentity(t *testing.T) { + const tenantID uint64 = 42 + ctx := withIMIdentity(context.Background(), tenantID) + + gotTenant, ok := types.TenantIDFromContext(ctx) + if !ok || gotTenant != tenantID { + t.Fatalf("TenantID = %d (ok=%v), want %d", gotTenant, ok, tenantID) + } + + userID, ok := types.UserIDFromContext(ctx) + if !ok || userID == "" { + t.Fatalf("UserID = %q (ok=%v), want non-empty synthetic user", userID, ok) + } + if want := "system-42"; userID != want { + t.Fatalf("UserID = %q, want %q", userID, want) + } + + // The synthetic shape must be recognised so RBAC code does not record it + // as a resource creator. + if !types.IsSyntheticUserID(userID) { + t.Fatalf("IsSyntheticUserID(%q) = false, want true", userID) + } + + // Non-empty UserID is the gate the shared-KB resolution relies on; without + // it Organization-shared KBs are silently skipped on the IM path. + if role := types.TenantRoleFromContext(ctx); role != types.TenantRoleViewer { + t.Fatalf("TenantRole = %v, want %v", role, types.TenantRoleViewer) + } +} diff --git a/internal/im/service.go b/internal/im/service.go index 75736e45..8426b132 100644 --- a/internal/im/service.go +++ b/internal/im/service.go @@ -450,6 +450,21 @@ func formatQuotedContext(quote *QuotedMessage) string { return label + "\n\n" + content + "\n" } +// withIMIdentity injects a synthetic caller identity into the context for IM +// callbacks. IM platforms verify their own signatures and bypass the auth +// middleware, so the downstream QA pipeline would otherwise see an empty +// UserID/TenantRole. Mirroring the API-key path's "system-" synthetic +// user (recognised by types.IsSyntheticUserID) lets Organization-shared +// knowledge bases be merged and resolved correctly, since the shared-KB code +// gates on a non-empty UserID. Viewer is the least privilege sufficient to +// retrieve shared KBs. +func withIMIdentity(ctx context.Context, tenantID uint64) context.Context { + ctx = context.WithValue(ctx, types.TenantIDContextKey, tenantID) + ctx = context.WithValue(ctx, types.UserIDContextKey, fmt.Sprintf("system-%d", tenantID)) + ctx = context.WithValue(ctx, types.TenantRoleContextKey, types.TenantRoleViewer) + return ctx +} + func buildIMQARequest( session *types.Session, query string, @@ -1126,8 +1141,8 @@ func (s *Service) HandleMessage(ctx context.Context, msg *IncomingMessage, chann if err != nil { return fmt.Errorf("get tenant: %w", err) } - sessionCtx := context.WithValue(ctx, types.TenantIDContextKey, tenantID) - sessionCtx = context.WithValue(sessionCtx, types.TenantInfoContextKey, tenant) + sessionCtx := context.WithValue(ctx, types.TenantInfoContextKey, tenant) + sessionCtx = withIMIdentity(sessionCtx, tenantID) // 2. Resolve or create a WeKnora session channelSession, err := s.resolveSession(sessionCtx, msg, tenantID, agentID, channelID, channel.SessionMode)