fix(im): inject synthetic identity so IM channels can use shared KBs
IM callback routes bypass the auth middleware because IM platforms verify their own signatures, so the downstream QA pipeline never received a UserID/TenantRole. The Organization-shared knowledge base resolution gates on a non-empty UserID, so IM-channel questions silently skipped shared KBs while the Web path (which sets UserID via JWT auth) worked. Inject a synthetic "system-<tenantID>" identity (mirroring the API-key path, recognised by types.IsSyntheticUserID) plus a Viewer role when building the IM session context. Viewer is the least privilege sufficient to retrieve shared KBs. Closes #1600
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
+17
-2
@@ -450,6 +450,21 @@ func formatQuotedContext(quote *QuotedMessage) string {
|
||||
return label + "\n<quoted_message>\n" + content + "\n</quoted_message>"
|
||||
}
|
||||
|
||||
// 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-<tenantID>" 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)
|
||||
|
||||
Reference in New Issue
Block a user