mirror of
https://github.com/Tencent/WeKnora.git
synced 2026-10-31 03:47:56 +08:00
Tighten correctness, security and observability of the tenant RBAC
foundation introduced in 3c4cdd70 in response to internal review.
Bugfixes
- custom_agents.runnable_by_viewer: drop the GORM `default:true` tag.
GORM treats a plain bool's zero value as "unspecified" and silently
rewrites it to the column default on insert, which would prevent
admins from ever creating an agent with viewer-runtime disabled. The
column-level DEFAULT TRUE on the database side still backstops rows
inserted by raw SQL / migrations.
- TenantMemberRepository.HasAnyMembers: replace `Limit(1).Count(&n)`
with `Take(&probe)`. GORM's Count ignores Limit and scans the whole
table, which matters because this runs on the auth middleware's
hot path for users without a cached membership.
- service/user.LoginWithOIDC: include User, Tenant and Memberships in
the OIDCCallbackResponse so the OIDC flow returns the same shape as
password login (previously User and friends were left nil).
Security
- middleware/auth.resolveTenantRole: restrict orphan-tenant
auto-promotion to the caller's home tenant. Cross-tenant superuser
visits and JWTs minted for foreign tenants no longer trigger silent
Owner grants. Two regression tests pin the new behaviour.
- service/user.synthFallbackMembership: default the synthesised role
to Viewer instead of Owner. The login response only feeds UI; the
backend re-derives the real role from tenant_members on every
request, so showing a Viewer UI is preferable to a misleading Owner
UI that would surface admin controls the backend will then 403.
- migrations/versioned/000043_tenant_rbac.down.sql: gate the destructive
drop behind a `weknora.allow_destructive_migration` GUC so an
accidental `migrate down` cannot wipe role data in production.
- frontend/src/stores/auth.ts: comment currentTenantRole as
rendering-only so future PRs cannot mistake the localStorage-backed
value for an authorisation signal.
API contract
- Memberships in LoginResponse and OIDCCallbackResponse now serialises
without omitempty, and buildMembershipsForUser / synthFallbackMembership
always return a non-nil slice — frontend can rely on JSON `[]`.
- handler/auth.GetCurrentUser includes memberships so /auth/me refreshes
recover currentTenantRole after a page reload.
Performance
- Add TenantService.GetTenantsByIDs (with the matching repo method) and
use it inside buildMembershipsForUser to batch tenant-name lookups
instead of issuing one query per membership.
Observability
- Replace the remaining `log.Printf` calls in middleware/auth with
contextual logger.Warnf / logger.Infof so role resolution events ride
the standard request-id / Langfuse trace plumbing. The successful
auto-promote line is tagged `[audit]` for log mining.
Tests
- internal/application/service/tenant_member_test.go: hand-rolled
fakeTenantMemberRepo plus 11 cases covering AddMember validation,
EnsureOwner idempotence, "cannot demote/remove the last Owner",
same-role no-op, ErrInvalidTenantRole / ErrMembershipNotFound, and
the TenantRole.HasPermission lattice.
- internal/middleware/auth_role_test.go: fakeMemberService plus 8 cases
pinning every branch of resolveTenantRole, including the two H1
regressions (cross-tenant superuser must not auto-promote; orphan
auto-promote requires the home tenant).
WeKnora HTTP Client
This package provides a client library for interacting with WeKnora services, supporting all HTTP-based interface calls, making it easier for other modules to integrate with WeKnora services without having to write HTTP request code directly.
Main Features
The client includes the following main functional modules:
- Session Management: Create, retrieve, update, and delete sessions
- Knowledge Base Management: Create, retrieve, update, and delete knowledge bases
- Knowledge Management: Add, retrieve, and delete knowledge content
- Tenant Management: CRUD operations for tenants
- Knowledge Q&A: Supports regular Q&A and streaming Q&A
- Chunk Management: Query, update, and delete knowledge chunks
- Message Management: Retrieve and delete session messages
- Model Management: Create, retrieve, update, and delete models
- Evaluation Function: Start evaluation tasks and get evaluation results
Usage
Creating Client Instance
import (
"context"
"github.com/Tencent/WeKnora/client"
"time"
)
// Create client instance
apiClient := client.NewClient(
"http://api.example.com",
client.WithToken("your-auth-token"),
client.WithTimeout(30*time.Second),
)
Tenant Configuration
You can set a default tenant with WithTenantID; the client will automatically send the X-Tenant-ID header:
tenantID := uint64(10000)
apiClient := client.NewClient(
"http://api.example.com",
client.WithToken("your-auth-token"),
client.WithTenantID(tenantID),
)
If a single request needs a different tenant, set TenantID in the request context. The value can be a uint64, *uint64, or a numeric string, and it will take precedence over the client default:
ctx := context.WithValue(context.Background(), "TenantID", uint64(10000))
// Pass ctx into any client method to switch to tenant 10000 for that request
Example: Create Knowledge Base and Upload File
// Create knowledge base
kb := &client.KnowledgeBase{
Name: "Test Knowledge Base",
Description: "This is a test knowledge base",
ChunkingConfig: client.ChunkingConfig{
ChunkSize: 500,
ChunkOverlap: 50,
Separators: []string{"\n\n", "\n", ". ", "? ", "! "},
},
ImageProcessingConfig: client.ImageProcessingConfig{
ModelID: "image_model_id",
},
EmbeddingModelID: "embedding_model_id",
SummaryModelID: "summary_model_id",
}
kb, err := apiClient.CreateKnowledgeBase(context.Background(), kb)
if err != nil {
// Handle error
}
// Upload knowledge file with metadata
metadata := map[string]string{
"source": "local",
"type": "document",
}
knowledge, err := apiClient.CreateKnowledgeFromFile(context.Background(), kb.ID, "path/to/file.pdf", metadata)
if err != nil {
// Handle error
}
Example: Create Session and Chat
// Create session
sessionRequest := &client.CreateSessionRequest{
KnowledgeBaseID: knowledgeBaseID,
SessionStrategy: &client.SessionStrategy{
MaxRounds: 10,
EnableRewrite: true,
FallbackStrategy: "fixed_answer",
FallbackResponse: "Sorry, I cannot answer this question",
EmbeddingTopK: 5,
KeywordThreshold: 0.5,
VectorThreshold: 0.7,
RerankModelID: "rerank_model_id",
RerankTopK: 3,
RerankThreshold: 0.8,
SummaryModelID: "summary_model_id",
},
}
session, err := apiClient.CreateSession(context.Background(), sessionRequest)
if err != nil {
// Handle error
}
// Regular Q&A
answer, err := apiClient.KnowledgeQA(context.Background(), session.ID, &client.KnowledgeQARequest{
Query: "What is artificial intelligence?",
})
if err != nil {
// Handle error
}
// Streaming Q&A
err = apiClient.KnowledgeQAStream(context.Background(), session.ID, "What is machine learning?", func(response *client.StreamResponse) error {
// Handle each response chunk
fmt.Print(response.Content)
return nil
})
if err != nil {
// Handle error
}
Example: Managing Models
// Create model
modelRequest := &client.CreateModelRequest{
Name: "Test Model",
Type: client.ModelTypeChat,
Source: client.ModelSourceInternal,
Description: "This is a test model",
Parameters: client.ModelParameters{
"temperature": 0.7,
"top_p": 0.9,
},
IsDefault: true,
}
model, err := apiClient.CreateModel(context.Background(), modelRequest)
if err != nil {
// Handle error
}
// List all models
models, err := apiClient.ListModels(context.Background())
if err != nil {
// Handle error
}
Example: Managing Knowledge Chunks
// List knowledge chunks
chunks, total, err := apiClient.ListKnowledgeChunks(context.Background(), knowledgeID, 1, 10)
if err != nil {
// Handle error
}
// Update chunk
updateRequest := &client.UpdateChunkRequest{
Content: "Updated chunk content",
IsEnabled: true,
}
updatedChunk, err := apiClient.UpdateChunk(context.Background(), knowledgeID, chunkID, updateRequest)
if err != nil {
// Handle error
}
Example: Getting Session Messages
// Get recent messages
messages, err := apiClient.GetRecentMessages(context.Background(), sessionID, 10)
if err != nil {
// Handle error
}
// Get messages before a specific time
beforeTime := time.Now().Add(-24 * time.Hour)
olderMessages, err := apiClient.GetMessagesBefore(context.Background(), sessionID, beforeTime, 10)
if err != nil {
// Handle error
}
Complete Example
Please refer to the ExampleUsage function in the example.go file, which demonstrates the complete usage flow of the client.