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,676 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/Tencent/WeKnora/internal/handler"
|
||||
sessionhandler "github.com/Tencent/WeKnora/internal/handler/session"
|
||||
"github.com/Tencent/WeKnora/internal/middleware"
|
||||
"github.com/Tencent/WeKnora/internal/types"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func TestConversationRoutesDeclareChatCapability(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
g := &rbacGuards{}
|
||||
v1 := gin.New().Group("/api/v1")
|
||||
|
||||
RegisterSessionRoutes(v1, &sessionhandler.Handler{}, &handler.MessageSuggestionHandler{}, g)
|
||||
RegisterChatRoutes(v1, &sessionhandler.Handler{}, g)
|
||||
RegisterMessageRoutes(v1, &handler.MessageHandler{}, g)
|
||||
|
||||
cases := []struct {
|
||||
method string
|
||||
path string
|
||||
}{
|
||||
{http.MethodPost, "/api/v1/sessions"},
|
||||
{http.MethodGet, "/api/v1/sessions/:id/messages/:message_id/suggestions"},
|
||||
{http.MethodPost, "/api/v1/sessions/:session_id/messages/:message_id/suggestions"},
|
||||
{http.MethodPost, "/api/v1/sessions/:session_id/suggestion-events"},
|
||||
{http.MethodPost, "/api/v1/knowledge-chat/:session_id"},
|
||||
{http.MethodPost, "/api/v1/agent-chat/:session_id"},
|
||||
{http.MethodGet, "/api/v1/messages/:session_id/load"},
|
||||
{http.MethodDelete, "/api/v1/messages/:session_id/:id"},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.method+" "+tc.path, func(t *testing.T) {
|
||||
policy := mustLookupAPIKeyPolicy(t, g, tc.method, tc.path)
|
||||
if !policy.RequireFullAccess {
|
||||
t.Fatal("policy should require full access without a matching capability")
|
||||
}
|
||||
if !policyHasCapability(policy, types.APIKeyCapabilityChat) {
|
||||
t.Fatalf("policy capabilities = %#v, want chat", policy.Capabilities)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlatformControlPlaneRoutesDeclarePlatformCapabilities(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
g := &rbacGuards{}
|
||||
v1 := gin.New().Group("/api/v1")
|
||||
RegisterSystemAdminRoutes(v1, &handler.SystemHandler{}, nil, g)
|
||||
|
||||
cases := []struct {
|
||||
method string
|
||||
path string
|
||||
capability types.APIKeyCapability
|
||||
}{
|
||||
{http.MethodGet, "/api/v1/system/admin/settings", types.APIKeyCapabilitySystemSettingsRead},
|
||||
{http.MethodPut, "/api/v1/system/admin/settings/:key", types.APIKeyCapabilitySystemSettingsManage},
|
||||
{http.MethodGet, "/api/v1/system/admin/runtime/queues", types.APIKeyCapabilitySystemRuntimeRead},
|
||||
{http.MethodPost, "/api/v1/system/admin/runtime/queues/:queue/tasks/:task_id/actions/:action", types.APIKeyCapabilitySystemRuntimeManage},
|
||||
{http.MethodDelete, "/api/v1/system/admin/runtime/queues/:queue/archived", types.APIKeyCapabilitySystemRuntimeManage},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
policy := mustLookupAPIKeyPolicy(t, g, tc.method, tc.path)
|
||||
if !policy.PlatformOnly {
|
||||
t.Fatalf("%s %s must be platform-only", tc.method, tc.path)
|
||||
}
|
||||
if !policyHasCapability(policy, tc.capability) {
|
||||
t.Fatalf("%s %s capabilities = %#v, want %s", tc.method, tc.path, policy.Capabilities, tc.capability)
|
||||
}
|
||||
}
|
||||
if _, ok := g.apiKeyAuthorizer.Lookup(http.MethodPost, "/api/v1/system/admin/api-keys"); ok {
|
||||
t.Fatal("platform API keys must not create other platform API keys")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlatformTenantLifecycleRoutesDeclarePlatformCapabilities(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
g := &rbacGuards{}
|
||||
v1 := gin.New().Group("/api/v1")
|
||||
RegisterTenantRoutes(
|
||||
v1,
|
||||
&handler.TenantHandler{},
|
||||
&handler.TenantMemberHandler{},
|
||||
&handler.TenantInvitationHandler{},
|
||||
nil,
|
||||
g,
|
||||
)
|
||||
|
||||
cases := []struct {
|
||||
method string
|
||||
path string
|
||||
capability types.APIKeyCapability
|
||||
}{
|
||||
{http.MethodGet, "/api/v1/tenants/all", types.APIKeyCapabilitySystemTenantsRead},
|
||||
{http.MethodGet, "/api/v1/tenants/search", types.APIKeyCapabilitySystemTenantsRead},
|
||||
{http.MethodPost, "/api/v1/tenants", types.APIKeyCapabilitySystemTenantsManage},
|
||||
{http.MethodGet, "/api/v1/tenants/:id", types.APIKeyCapabilitySystemTenantsRead},
|
||||
{http.MethodPut, "/api/v1/tenants/:id", types.APIKeyCapabilitySystemTenantsManage},
|
||||
{http.MethodDelete, "/api/v1/tenants/:id", types.APIKeyCapabilitySystemTenantsManage},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
policy := mustLookupAPIKeyPolicy(t, g, tc.method, tc.path)
|
||||
if !policy.PlatformOnly {
|
||||
t.Fatalf("%s %s must be platform-only", tc.method, tc.path)
|
||||
}
|
||||
if !policyHasCapability(policy, tc.capability) {
|
||||
t.Fatalf("%s %s capabilities = %#v, want %s", tc.method, tc.path, policy.Capabilities, tc.capability)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMessageHistoryRoutesDeclareMessageHistoryCapability(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
g := &rbacGuards{}
|
||||
v1 := gin.New().Group("/api/v1")
|
||||
|
||||
RegisterMessageRoutes(v1, &handler.MessageHandler{}, g)
|
||||
|
||||
cases := []struct {
|
||||
method string
|
||||
path string
|
||||
}{
|
||||
{http.MethodPost, "/api/v1/messages/search"},
|
||||
{http.MethodGet, "/api/v1/messages/chat-history-stats"},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.method+" "+tc.path, func(t *testing.T) {
|
||||
policy := mustLookupAPIKeyPolicy(t, g, tc.method, tc.path)
|
||||
if !policy.RequireFullAccess {
|
||||
t.Fatal("policy should require full access without a matching capability")
|
||||
}
|
||||
if !policyHasCapability(policy, types.APIKeyCapabilityMessageHistory) {
|
||||
t.Fatalf("policy capabilities = %#v, want message_history", policy.Capabilities)
|
||||
}
|
||||
if policyHasCapability(policy, types.APIKeyCapabilityChat) {
|
||||
t.Fatalf("message-history route must not be granted by chat: %#v", policy.Capabilities)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgentReadRoutesDeclareReadAgentsCapability(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
g := &rbacGuards{}
|
||||
v1 := gin.New().Group("/api/v1")
|
||||
|
||||
RegisterCustomAgentRoutes(v1, &handler.CustomAgentHandler{}, g)
|
||||
|
||||
cases := []struct {
|
||||
method string
|
||||
path string
|
||||
}{
|
||||
{http.MethodGet, "/api/v1/agents/placeholders"},
|
||||
{http.MethodGet, "/api/v1/agents/type-presets"},
|
||||
{http.MethodGet, "/api/v1/agents"},
|
||||
{http.MethodGet, "/api/v1/agents/:id"},
|
||||
{http.MethodGet, "/api/v1/agents/:id/suggested-questions"},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.method+" "+tc.path, func(t *testing.T) {
|
||||
policy := mustLookupAPIKeyPolicy(t, g, tc.method, tc.path)
|
||||
if !policy.RequireFullAccess {
|
||||
t.Fatal("policy should require full access without a matching capability")
|
||||
}
|
||||
if !policyHasCapability(policy, types.APIKeyCapabilityReadAgents) {
|
||||
t.Fatalf("policy capabilities = %#v, want read_agents", policy.Capabilities)
|
||||
}
|
||||
if !policyHasCapability(policy, types.APIKeyCapabilityChat) {
|
||||
t.Fatalf("policy capabilities = %#v, want chat for conversation clients", policy.Capabilities)
|
||||
}
|
||||
if !policyHasCapability(policy, types.APIKeyCapabilityManageAgents) {
|
||||
t.Fatalf("policy capabilities = %#v, want manage_agents for authoring clients", policy.Capabilities)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgentWriteRoutesRequireManageAgentsCapability(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
g := &rbacGuards{}
|
||||
v1 := gin.New().Group("/api/v1")
|
||||
|
||||
RegisterCustomAgentRoutes(v1, &handler.CustomAgentHandler{}, g)
|
||||
|
||||
cases := []struct {
|
||||
method string
|
||||
path string
|
||||
}{
|
||||
{http.MethodPost, "/api/v1/agents"},
|
||||
{http.MethodPut, "/api/v1/agents/:id"},
|
||||
{http.MethodDelete, "/api/v1/agents/:id"},
|
||||
{http.MethodPost, "/api/v1/agents/:id/copy"},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.method+" "+tc.path, func(t *testing.T) {
|
||||
policy := mustLookupAPIKeyPolicy(t, g, tc.method, tc.path)
|
||||
if !policy.RequireFullAccess {
|
||||
t.Fatal("policy should require full access without a matching capability")
|
||||
}
|
||||
if !policyHasCapability(policy, types.APIKeyCapabilityManageAgents) {
|
||||
t.Fatalf("policy capabilities = %#v, want manage_agents", policy.Capabilities)
|
||||
}
|
||||
if policyHasCapability(policy, types.APIKeyCapabilityReadAgents) {
|
||||
t.Fatalf("agent write route must not be granted by read_agents: %#v", policy.Capabilities)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestKnowledgeBaseManagementRoutesDeclareManageKBsCapability(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
g := &rbacGuards{}
|
||||
v1 := gin.New().Group("/api/v1")
|
||||
|
||||
RegisterKnowledgeBaseRoutes(v1, &handler.KnowledgeBaseHandler{}, g)
|
||||
RegisterInitializationRoutes(v1, &handler.InitializationHandler{}, g)
|
||||
|
||||
cases := []struct {
|
||||
method string
|
||||
path string
|
||||
}{
|
||||
{http.MethodPut, "/api/v1/knowledge-bases/:id"},
|
||||
{http.MethodDelete, "/api/v1/knowledge-bases/:id"},
|
||||
{http.MethodPost, "/api/v1/initialization/initialize/:kbId"},
|
||||
{http.MethodPut, "/api/v1/initialization/config/:kbId"},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.method+" "+tc.path, func(t *testing.T) {
|
||||
policy := mustLookupAPIKeyPolicy(t, g, tc.method, tc.path)
|
||||
if !policy.RequireFullAccess {
|
||||
t.Fatal("policy should require full access without a matching capability")
|
||||
}
|
||||
if !policyHasCapability(policy, types.APIKeyCapabilityManageKnowledgeBases) {
|
||||
t.Fatalf("policy capabilities = %#v, want manage_kbs", policy.Capabilities)
|
||||
}
|
||||
if policyHasCapability(policy, types.APIKeyCapabilityIngest) {
|
||||
t.Fatalf("KB management route must not be granted by ingest: %#v", policy.Capabilities)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestKnowledgeBaseLifecycleRoutesDeclareManageCapability(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
g := &rbacGuards{}
|
||||
v1 := gin.New().Group("/api/v1")
|
||||
|
||||
RegisterKnowledgeBaseRoutes(v1, &handler.KnowledgeBaseHandler{}, g)
|
||||
|
||||
// The whole KB lifecycle (create/copy/duplicate/update/delete) shares one
|
||||
// policy tier: manage_kbs OR full-access. create/copy/duplicate produce a
|
||||
// new KB but are still KB-management operations, so manage_kbs admits them
|
||||
// (the allow-list bounds copy/duplicate/update/delete downstream; ingest
|
||||
// must never grant any of these).
|
||||
cases := []struct {
|
||||
method string
|
||||
path string
|
||||
}{
|
||||
{http.MethodPost, "/api/v1/knowledge-bases"},
|
||||
{http.MethodPost, "/api/v1/knowledge-bases/copy"},
|
||||
{http.MethodPost, "/api/v1/knowledge-bases/:id/duplicate"},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.method+" "+tc.path, func(t *testing.T) {
|
||||
policy := mustLookupAPIKeyPolicy(t, g, tc.method, tc.path)
|
||||
if !policy.RequireFullAccess {
|
||||
t.Fatal("policy should require full access without a matching capability")
|
||||
}
|
||||
if !policyHasCapability(policy, types.APIKeyCapabilityManageKnowledgeBases) {
|
||||
t.Fatalf("policy capabilities = %#v, want manage_kbs", policy.Capabilities)
|
||||
}
|
||||
if policyHasCapability(policy, types.APIKeyCapabilityIngest) {
|
||||
t.Fatalf("KB lifecycle route must not be granted by ingest: %#v", policy.Capabilities)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestKnowledgeReadRoutesDeclareRetrieveCapability(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
g := &rbacGuards{}
|
||||
v1 := gin.New().Group("/api/v1")
|
||||
|
||||
RegisterKnowledgeBaseRoutes(v1, &handler.KnowledgeBaseHandler{}, g)
|
||||
RegisterKnowledgeRoutes(v1, &handler.KnowledgeHandler{}, g)
|
||||
RegisterFAQRoutes(v1, &handler.FAQHandler{}, g)
|
||||
RegisterKnowledgeTagRoutes(v1, &handler.TagHandler{}, g)
|
||||
RegisterChatRoutes(v1, &sessionhandler.Handler{}, g)
|
||||
RegisterInitializationRoutes(v1, &handler.InitializationHandler{}, g)
|
||||
RegisterWikiPageRoutes(v1, &handler.WikiPageHandler{}, g)
|
||||
|
||||
cases := []struct {
|
||||
method string
|
||||
path string
|
||||
}{
|
||||
{http.MethodGet, "/api/v1/knowledge-bases"},
|
||||
{http.MethodGet, "/api/v1/knowledge-bases/:id"},
|
||||
{http.MethodPost, "/api/v1/knowledge-bases/:id/hybrid-search"},
|
||||
{http.MethodGet, "/api/v1/knowledge-bases/:id/knowledge"},
|
||||
{http.MethodGet, "/api/v1/knowledge/:id"},
|
||||
{http.MethodGet, "/api/v1/knowledge/:id/download"},
|
||||
{http.MethodPost, "/api/v1/knowledge-bases/:id/faq/search"},
|
||||
{http.MethodGet, "/api/v1/knowledge-bases/:id/tags"},
|
||||
{http.MethodPost, "/api/v1/knowledge-search"},
|
||||
{http.MethodGet, "/api/v1/initialization/config/:kbId"},
|
||||
{http.MethodGet, "/api/v1/knowledgebase/:kb_id/wiki/pages"},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.method+" "+tc.path, func(t *testing.T) {
|
||||
policy := mustLookupAPIKeyPolicy(t, g, tc.method, tc.path)
|
||||
if !policy.RequireFullAccess {
|
||||
t.Fatal("policy should require full access without a matching capability")
|
||||
}
|
||||
if !policyHasCapability(policy, types.APIKeyCapabilityRetrieve) {
|
||||
t.Fatalf("policy capabilities = %#v, want retrieve", policy.Capabilities)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTenantInfrastructureRoutesDeclareSpecificCapabilities(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
g := &rbacGuards{}
|
||||
v1 := gin.New().Group("/api/v1")
|
||||
|
||||
RegisterTenantRoutes(v1, &handler.TenantHandler{}, nil, nil, nil, g)
|
||||
RegisterModelRoutes(v1, &handler.ModelHandler{}, &handler.ModelCredentialsHandler{}, g)
|
||||
RegisterEvaluationRoutes(v1, &handler.EvaluationHandler{}, g)
|
||||
RegisterSystemRoutes(v1, &handler.SystemHandler{}, g)
|
||||
RegisterMCPServiceRoutes(v1, &handler.MCPServiceHandler{}, &handler.MCPCredentialsHandler{}, &handler.MCPOAuthHandler{}, g)
|
||||
RegisterWebSearchProviderRoutes(v1, &handler.WebSearchProviderHandler{}, &handler.WebSearchProviderCredentialsHandler{}, g)
|
||||
RegisterVectorStoreRoutes(v1, &handler.VectorStoreHandler{}, g)
|
||||
RegisterStorageBackendRoutes(v1, &handler.StorageBackendHandler{}, g)
|
||||
RegisterSandboxConfigRoutes(v1, &handler.SandboxConfigHandler{}, &handler.SandboxSkillHandler{}, g)
|
||||
RegisterEmbedChannelRoutes(v1, &handler.EmbedChannelHandler{}, g)
|
||||
RegisterIMChannelRoutes(v1, &handler.IMHandler{}, g)
|
||||
RegisterDataSourceRoutes(v1, &handler.DataSourceHandler{}, &handler.DataSourceCredentialsHandler{}, g)
|
||||
RegisterWeKnoraCloudRoutes(v1, &handler.WeKnoraCloudHandler{}, g)
|
||||
|
||||
capabilitiesPolicy := mustLookupAPIKeyPolicy(t, g, http.MethodGet, "/api/v1/system/capabilities")
|
||||
if capabilitiesPolicy.RequireFullAccess || len(capabilitiesPolicy.Capabilities) != 0 {
|
||||
t.Fatalf("system capabilities should be readable by any valid API key: %#v", capabilitiesPolicy)
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
method string
|
||||
path string
|
||||
cap types.APIKeyCapability
|
||||
}{
|
||||
{http.MethodGet, "/api/v1/tenants", types.APIKeyCapabilityManageTenantSettings},
|
||||
{http.MethodGet, "/api/v1/models", types.APIKeyCapabilityManageModels},
|
||||
{http.MethodDelete, "/api/v1/models/:id", types.APIKeyCapabilityManageModels},
|
||||
{http.MethodPost, "/api/v1/evaluation", types.APIKeyCapabilityRunEvaluations},
|
||||
{http.MethodGet, "/api/v1/system/info", types.APIKeyCapabilityManageVectorStores},
|
||||
{http.MethodGet, "/api/v1/mcp-services", types.APIKeyCapabilityManageMCPServices},
|
||||
{http.MethodGet, "/api/v1/web-search-providers", types.APIKeyCapabilityManageWebSearch},
|
||||
{http.MethodGet, "/api/v1/vector-stores", types.APIKeyCapabilityManageVectorStores},
|
||||
{http.MethodGet, "/api/v1/storage-backends", types.APIKeyCapabilityManageStorageBackends},
|
||||
{http.MethodGet, "/api/v1/embed-channels", types.APIKeyCapabilityManageChannels},
|
||||
{http.MethodGet, "/api/v1/im-channels", types.APIKeyCapabilityManageChannels},
|
||||
{http.MethodGet, "/api/v1/datasource", types.APIKeyCapabilityManageDataSources},
|
||||
{http.MethodGet, "/api/v1/models/weknoracloud/status", types.APIKeyCapabilityManageModels},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.method+" "+tc.path, func(t *testing.T) {
|
||||
policy := mustLookupAPIKeyPolicy(t, g, tc.method, tc.path)
|
||||
if !policy.RequireFullAccess {
|
||||
t.Fatal("policy should require full access without a matching capability")
|
||||
}
|
||||
if !policyHasCapability(policy, tc.cap) {
|
||||
t.Fatalf("policy capabilities = %#v, want %s", policy.Capabilities, tc.cap)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSandboxConfigRoutesRequireFullAccessOnly(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
g := &rbacGuards{}
|
||||
v1 := gin.New().Group("/api/v1")
|
||||
|
||||
RegisterSandboxConfigRoutes(v1, &handler.SandboxConfigHandler{}, &handler.SandboxSkillHandler{}, g)
|
||||
|
||||
cases := []struct {
|
||||
method string
|
||||
path string
|
||||
}{
|
||||
{http.MethodGet, "/api/v1/sandbox-configs"},
|
||||
{http.MethodPost, "/api/v1/sandbox-configs"},
|
||||
{http.MethodPost, "/api/v1/sandbox-configs/templates/query"},
|
||||
{http.MethodGet, "/api/v1/sandbox-configs/:id"},
|
||||
{http.MethodPut, "/api/v1/sandbox-configs/:id"},
|
||||
{http.MethodDelete, "/api/v1/sandbox-configs/:id"},
|
||||
{http.MethodGet, "/api/v1/sandbox-configs/:id/sandboxes"},
|
||||
{http.MethodGet, "/api/v1/sandbox-configs/:id/skills"},
|
||||
{http.MethodPost, "/api/v1/sandbox-configs/:id/skills"},
|
||||
{http.MethodGet, "/api/v1/sandbox-configs/:id/skills/:skillId"},
|
||||
{http.MethodGet, "/api/v1/sandbox-configs/:id/skills/:skillId/files"},
|
||||
{http.MethodGet, "/api/v1/sandbox-configs/:id/skills/:skillId/files/content"},
|
||||
{http.MethodPost, "/api/v1/sandbox-configs/:id/skills/:skillId/reinstall"},
|
||||
{http.MethodPost, "/api/v1/sandbox-configs/:id/skills/:skillId/stop"},
|
||||
{http.MethodPatch, "/api/v1/sandbox-configs/:id/skills/:skillId"},
|
||||
{http.MethodDelete, "/api/v1/sandbox-configs/:id/skills/:skillId"},
|
||||
{http.MethodGet, "/api/v1/sandbox-configs/:id/skills/:skillId/install-events"},
|
||||
// The transcript replays the root shell session that built the image,
|
||||
// so it must not be reachable by a scoped key either.
|
||||
{http.MethodGet, "/api/v1/sandbox-configs/:id/skills/:skillId/transcript"},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.method+" "+tc.path, func(t *testing.T) {
|
||||
policy := mustLookupAPIKeyPolicy(t, g, tc.method, tc.path)
|
||||
if !policy.RequireFullAccess {
|
||||
t.Fatal("sandbox config routes should require full access")
|
||||
}
|
||||
if len(policy.Capabilities) != 0 {
|
||||
t.Fatalf("sandbox config routes must not be granted by a scoped capability: %#v", policy.Capabilities)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSkillCatalogWriteRoutesRequireFullAccess(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
g := &rbacGuards{}
|
||||
v1 := gin.New().Group("/api/v1")
|
||||
RegisterSkillRoutes(v1, &handler.SkillHandler{}, g)
|
||||
|
||||
cases := []struct {
|
||||
method string
|
||||
path string
|
||||
}{
|
||||
{http.MethodPost, "/api/v1/skills/catalog"},
|
||||
{http.MethodPost, "/api/v1/skills/catalog/:id/install"},
|
||||
{http.MethodGet, "/api/v1/skills/catalog/:id/files"},
|
||||
{http.MethodGet, "/api/v1/skills/catalog/:id/files/content"},
|
||||
{http.MethodDelete, "/api/v1/skills/catalog/:id"},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.method+" "+tc.path, func(t *testing.T) {
|
||||
policy := mustLookupAPIKeyPolicy(t, g, tc.method, tc.path)
|
||||
if !policy.RequireFullAccess {
|
||||
t.Fatal("catalog writes bake into sandbox images and must require full access")
|
||||
}
|
||||
if len(policy.Capabilities) != 0 {
|
||||
t.Fatalf("catalog writes must not be granted by a scoped capability: %#v", policy.Capabilities)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTenantMemberRoutesDeclareManageMembersCapability(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
g := &rbacGuards{}
|
||||
v1 := gin.New().Group("/api/v1")
|
||||
|
||||
RegisterTenantRoutes(v1, &handler.TenantHandler{}, &handler.TenantMemberHandler{}, &handler.TenantInvitationHandler{}, nil, g)
|
||||
|
||||
cases := []struct {
|
||||
method string
|
||||
path string
|
||||
}{
|
||||
{http.MethodGet, "/api/v1/tenants/:id/members"},
|
||||
{http.MethodPost, "/api/v1/tenants/:id/members"},
|
||||
{http.MethodPut, "/api/v1/tenants/:id/members/:user_id"},
|
||||
{http.MethodDelete, "/api/v1/tenants/:id/members/:user_id"},
|
||||
{http.MethodGet, "/api/v1/tenants/:id/invitations"},
|
||||
{http.MethodPost, "/api/v1/tenants/:id/invitations"},
|
||||
{http.MethodDelete, "/api/v1/tenants/:id/invitations/:inv_id"},
|
||||
{http.MethodPost, "/api/v1/tenants/:id/invite-links"},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.method+" "+tc.path, func(t *testing.T) {
|
||||
policy := mustLookupAPIKeyPolicy(t, g, tc.method, tc.path)
|
||||
if !policy.RequireFullAccess {
|
||||
t.Fatal("policy should require full access without a matching capability")
|
||||
}
|
||||
if !policyHasCapability(policy, types.APIKeyCapabilityManageMembers) {
|
||||
t.Fatalf("policy capabilities = %#v, want manage_members", policy.Capabilities)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if _, ok := g.apiKeyAuthorizer.Lookup(http.MethodPost, "/api/v1/tenants/:id/leave"); ok {
|
||||
t.Fatal("tenant leave route should remain default-deny for API keys")
|
||||
}
|
||||
}
|
||||
|
||||
func TestOrganizationRoutesDeclareManageSpacesCapability(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
g := &rbacGuards{}
|
||||
v1 := gin.New().Group("/api/v1")
|
||||
|
||||
RegisterOrganizationRoutes(v1, &handler.OrganizationHandler{}, g)
|
||||
|
||||
cases := []struct {
|
||||
method string
|
||||
path string
|
||||
}{
|
||||
{http.MethodPost, "/api/v1/organizations"},
|
||||
{http.MethodGet, "/api/v1/organizations"},
|
||||
{http.MethodPost, "/api/v1/organizations/join"},
|
||||
{http.MethodGet, "/api/v1/organizations/search"},
|
||||
{http.MethodPut, "/api/v1/organizations/:id"},
|
||||
{http.MethodPost, "/api/v1/organizations/:id/invite-code"},
|
||||
{http.MethodGet, "/api/v1/organizations/:id/members"},
|
||||
{http.MethodPut, "/api/v1/organizations/:id/members/:tenant_id"},
|
||||
{http.MethodGet, "/api/v1/shared-knowledge-bases"},
|
||||
{http.MethodGet, "/api/v1/shared-agents"},
|
||||
{http.MethodPost, "/api/v1/shared-agents/disabled"},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.method+" "+tc.path, func(t *testing.T) {
|
||||
policy := mustLookupAPIKeyPolicy(t, g, tc.method, tc.path)
|
||||
if !policy.RequireFullAccess {
|
||||
t.Fatal("policy should require full access without a matching capability")
|
||||
}
|
||||
if !policyHasCapability(policy, types.APIKeyCapabilityManageSpaces) {
|
||||
t.Fatalf("policy capabilities = %#v, want manage_spaces", policy.Capabilities)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// KB/agent share management is open to full-access keys (tenant-wide
|
||||
// authority) but never via a capability.
|
||||
shareRoutes := []struct {
|
||||
method string
|
||||
path string
|
||||
}{
|
||||
{http.MethodPost, "/api/v1/knowledge-bases/:id/shares"},
|
||||
{http.MethodGet, "/api/v1/knowledge-bases/:id/shares"},
|
||||
{http.MethodPut, "/api/v1/knowledge-bases/:id/shares/:share_id"},
|
||||
{http.MethodDelete, "/api/v1/knowledge-bases/:id/shares/:share_id"},
|
||||
{http.MethodPost, "/api/v1/agents/:id/shares"},
|
||||
{http.MethodGet, "/api/v1/agents/:id/shares"},
|
||||
{http.MethodDelete, "/api/v1/agents/:id/shares/:share_id"},
|
||||
}
|
||||
for _, tc := range shareRoutes {
|
||||
t.Run("share "+tc.method+" "+tc.path, func(t *testing.T) {
|
||||
policy := mustLookupAPIKeyPolicy(t, g, tc.method, tc.path)
|
||||
if !policy.RequireFullAccess {
|
||||
t.Fatal("share route should require full access for API keys")
|
||||
}
|
||||
if len(policy.Capabilities) != 0 {
|
||||
t.Fatalf("share route must not be granted by any capability: %#v", policy.Capabilities)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestChunkerPreviewRouteRequiresRetrieveOrIngestCapability(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
g := &rbacGuards{}
|
||||
v1 := gin.New().Group("/api/v1")
|
||||
|
||||
RegisterChunkerDebugRoutes(v1, g)
|
||||
|
||||
policy := mustLookupAPIKeyPolicy(t, g, http.MethodPost, "/api/v1/chunker/preview")
|
||||
if !policy.RequireFullAccess {
|
||||
t.Fatal("policy should require full access without a matching capability")
|
||||
}
|
||||
if !policyHasCapability(policy, types.APIKeyCapabilityRetrieve) {
|
||||
t.Fatalf("policy capabilities = %#v, want retrieve", policy.Capabilities)
|
||||
}
|
||||
if !policyHasCapability(policy, types.APIKeyCapabilityIngest) {
|
||||
t.Fatalf("policy capabilities = %#v, want ingest", policy.Capabilities)
|
||||
}
|
||||
}
|
||||
|
||||
// The batch / cross-KB content-write routes bind themselves to a single (or
|
||||
// source+target) KB and enforce the API key's KB allow-list downstream, so
|
||||
// they are reachable by an ingest-capable (or full-access) key — matching
|
||||
// their single-document siblings.
|
||||
func TestKnowledgeBatchWriteRoutesDeclareIngestCapability(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
g := &rbacGuards{}
|
||||
v1 := gin.New().Group("/api/v1")
|
||||
|
||||
RegisterKnowledgeRoutes(v1, &handler.KnowledgeHandler{}, g)
|
||||
|
||||
cases := []struct {
|
||||
method string
|
||||
path string
|
||||
}{
|
||||
{http.MethodPost, "/api/v1/knowledge/move"},
|
||||
{http.MethodPost, "/api/v1/knowledge/batch-delete"},
|
||||
{http.MethodPost, "/api/v1/knowledge/batch-reparse"},
|
||||
{http.MethodPut, "/api/v1/knowledge/tags"},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.method+" "+tc.path, func(t *testing.T) {
|
||||
policy := mustLookupAPIKeyPolicy(t, g, tc.method, tc.path)
|
||||
if !policy.RequireFullAccess {
|
||||
t.Fatal("policy should require full access without a matching capability")
|
||||
}
|
||||
if !policyHasCapability(policy, types.APIKeyCapabilityIngest) {
|
||||
t.Fatalf("policy capabilities = %#v, want ingest", policy.Capabilities)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestKBCloneProgressRouteRequiresRetrieveOrManageKbsCapability(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
g := &rbacGuards{}
|
||||
v1 := gin.New().Group("/api/v1")
|
||||
|
||||
RegisterKnowledgeBaseRoutes(v1, &handler.KnowledgeBaseHandler{}, g)
|
||||
|
||||
policy := mustLookupAPIKeyPolicy(t, g, http.MethodGet, "/api/v1/knowledge-bases/copy/progress/:task_id")
|
||||
if !policy.RequireFullAccess {
|
||||
t.Fatal("policy should require full access without a matching capability")
|
||||
}
|
||||
if !policyHasCapability(policy, types.APIKeyCapabilityRetrieve) {
|
||||
t.Fatalf("policy capabilities = %#v, want retrieve", policy.Capabilities)
|
||||
}
|
||||
if !policyHasCapability(policy, types.APIKeyCapabilityManageKnowledgeBases) {
|
||||
t.Fatalf("policy capabilities = %#v, want manage_kbs", policy.Capabilities)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFAQImportProgressRouteRequiresRetrieveOrIngestCapability(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
g := &rbacGuards{}
|
||||
v1 := gin.New().Group("/api/v1")
|
||||
|
||||
RegisterFAQRoutes(v1, &handler.FAQHandler{}, g)
|
||||
|
||||
policy := mustLookupAPIKeyPolicy(t, g, http.MethodGet, "/api/v1/faq/import/progress/:task_id")
|
||||
if !policy.RequireFullAccess {
|
||||
t.Fatal("policy should require full access without a matching capability")
|
||||
}
|
||||
if !policyHasCapability(policy, types.APIKeyCapabilityRetrieve) {
|
||||
t.Fatalf("policy capabilities = %#v, want retrieve", policy.Capabilities)
|
||||
}
|
||||
if !policyHasCapability(policy, types.APIKeyCapabilityIngest) {
|
||||
t.Fatalf("policy capabilities = %#v, want ingest", policy.Capabilities)
|
||||
}
|
||||
}
|
||||
|
||||
func mustLookupAPIKeyPolicy(
|
||||
t *testing.T,
|
||||
g *rbacGuards,
|
||||
method string,
|
||||
path string,
|
||||
) middleware.APIKeyRoutePolicy {
|
||||
t.Helper()
|
||||
policy, ok := g.apiKeyAuthorizer.Lookup(method, path)
|
||||
if !ok {
|
||||
t.Fatalf("missing API-key policy for %s %s", method, path)
|
||||
}
|
||||
return policy
|
||||
}
|
||||
|
||||
func policyHasCapability(policy middleware.APIKeyRoutePolicy, cap types.APIKeyCapability) bool {
|
||||
for _, got := range policy.Capabilities {
|
||||
if got == cap {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
Reference in New Issue
Block a user