Files
WeKnora/internal/errors/errors.go
T
wizardchen 8efe1b4187 refactor(rbac): address kb-access guard review findings
Follow-up to the route-level KB-access guard introduced earlier in
this PR. Five reviewer findings, all fixed in middleware + tests
without changing handler call sites:

  - Honour `?agent_id` in the agent-share fallback. The in-handler
    resolution in knowledgebase.go branches on a specific agent's
    KBSelectionMode (all / selected / none); the guard was only ever
    calling TenantCanAccessKBViaSomeSharedAgent, which let some
    requests pass at the guard layer that the handler would then
    reject — and rewrote the request's tenant context to the source
    tenant before the rejection happened. The guard now mirrors the
    handler: a specific agent_id pins the resolution to that agent
    (no fallback to any-agent), an empty agent_id keeps the existing
    any-agent behaviour. The knowledge.go / knowledgebase.go handler
    helpers stay in place as defence in depth until they're folded
    into the guard in a follow-up.

  - Distinguish not-found vs. transient on the knowledge / chunk
    resolvers. A DB hiccup used to surface as a 404, both confusing
    clients and hiding the underlying failure from monitoring. The
    resolvers now return apperror 404 only for the known not-found
    sentinels (apprepo.ErrKnowledgeBaseNotFound, ErrResourceNotFound)
    and let other errors propagate; the guard maps those to a fresh
    NewServiceUnavailableError (503).

  - Stop 500-ing on legacy chunks with empty knowledge_base_id. The
    chunk is effectively unresolvable to a KB, so the client gets the
    same 404 they'd get for a missing chunk instead of a 500 that
    pollutes alerting. The warn log preserves the operator signal.

  - Honour cfg.Tenant.EnableRBAC. The guard now mirrors RequireRole /
    RequireOwnershipOrRole: with enforcement off, 401/403 paths log
    the would-be rejection and pass through. 404 still fires either
    way (a missing resource is not an authorisation event). Keeps the
    rollout window safe — operators can flip enforcement on globally
    without code changes elsewhere.

  - Document the c.Keys vs. c.Request.Context() split. The guard
    intentionally rewrites only the request context (handlers that
    still run their own share resolution, currently knowledge.go /
    knowledgebase.go, would otherwise see kb.TenantID == c.Keys
    tenant and misclassify shared-Viewer access as Admin). The
    package-level comment now spells this out so the migration of the
    remaining handlers in a follow-up doesn't accidentally regress
    `my_permission` rendering.

Tests:

  - Existing four cases keep working; runGuard signature gained a
    guardOpts knob.
  - Eight new cases cover the agent-share resolution paths (any-agent
    viewer, Editor required → must reject, specific agent in modes
    all / selected-match / selected-miss / none, cross-tenant
    mismatch) plus the EnableRBAC=false fail-open + 404-still-fires
    pair.
2026-05-18 17:28:58 +08:00

243 lines
6.5 KiB
Go

package errors
import (
"fmt"
"net/http"
)
// ErrorCode defines the error code type
type ErrorCode int
// System error codes
const (
// Common error codes (1000-1999)
ErrBadRequest ErrorCode = 1000
ErrUnauthorized ErrorCode = 1001
ErrForbidden ErrorCode = 1002
ErrNotFound ErrorCode = 1003
ErrMethodNotAllowed ErrorCode = 1004
ErrConflict ErrorCode = 1005
ErrTooManyRequests ErrorCode = 1006
ErrInternalServer ErrorCode = 1007
ErrServiceUnavailable ErrorCode = 1008
ErrTimeout ErrorCode = 1009
ErrValidation ErrorCode = 1010
// Tenant related error codes (2000-2099)
ErrTenantNotFound ErrorCode = 2000
ErrTenantAlreadyExists ErrorCode = 2001
ErrTenantInactive ErrorCode = 2002
ErrTenantNameRequired ErrorCode = 2003
ErrTenantInvalidStatus ErrorCode = 2004
// Agent related error codes (2100-2199)
ErrAgentMissingThinkingModel ErrorCode = 2100
ErrAgentMissingAllowedTools ErrorCode = 2101
ErrAgentInvalidMaxIterations ErrorCode = 2102
ErrAgentInvalidTemperature ErrorCode = 2103
// VectorStore binding related error codes (2200-2299).
// Both map to HTTP 400 with a generic message; the typed code lets
// clients distinguish "wrong UUID / cross-tenant" from "store exists
// but is currently unavailable" without parsing the message text.
ErrVectorStoreBindingInvalid ErrorCode = 2200
ErrVectorStoreUnavailable ErrorCode = 2201
// Add more error codes here
)
// AppError defines the application error structure
type AppError struct {
Code ErrorCode `json:"code"`
Message string `json:"message"`
Details any `json:"details,omitempty"`
HTTPCode int `json:"-"`
}
// Error implements the error interface
func (e *AppError) Error() string {
return fmt.Sprintf("error code: %d, error message: %s", e.Code, e.Message)
}
// WithDetails adds error details
func (e *AppError) WithDetails(details any) *AppError {
e.Details = details
return e
}
// NewBadRequestError creates a bad request error
func NewBadRequestError(message string) *AppError {
return &AppError{
Code: ErrBadRequest,
Message: message,
HTTPCode: http.StatusBadRequest,
}
}
// NewUnauthorizedError creates an unauthorized error
func NewUnauthorizedError(message string) *AppError {
return &AppError{
Code: ErrUnauthorized,
Message: message,
HTTPCode: http.StatusUnauthorized,
}
}
// NewForbiddenError creates a forbidden error
func NewForbiddenError(message string) *AppError {
return &AppError{
Code: ErrForbidden,
Message: message,
HTTPCode: http.StatusForbidden,
}
}
// NewNotFoundError creates a not found error
func NewNotFoundError(message string) *AppError {
return &AppError{
Code: ErrNotFound,
Message: message,
HTTPCode: http.StatusNotFound,
}
}
// NewConflictError creates a conflict error
func NewConflictError(message string) *AppError {
return &AppError{
Code: ErrConflict,
Message: message,
HTTPCode: http.StatusConflict,
}
}
// NewInternalServerError creates an internal server error
func NewInternalServerError(message string) *AppError {
if message == "" {
message = "服务器内部错误"
}
return &AppError{
Code: ErrInternalServer,
Message: message,
HTTPCode: http.StatusInternalServerError,
}
}
// NewServiceUnavailableError creates a service unavailable (503)
// error. Used for transient failures where the caller can retry.
func NewServiceUnavailableError(message string) *AppError {
if message == "" {
message = "服务暂时不可用"
}
return &AppError{
Code: ErrServiceUnavailable,
Message: message,
HTTPCode: http.StatusServiceUnavailable,
}
}
// NewValidationError creates a validation error
func NewValidationError(message string) *AppError {
return &AppError{
Code: ErrValidation,
Message: message,
HTTPCode: http.StatusBadRequest,
}
}
// Tenant related errors
func NewTenantNotFoundError() *AppError {
return &AppError{
Code: ErrTenantNotFound,
Message: "租户不存在",
HTTPCode: http.StatusNotFound,
}
}
// NewTenantAlreadyExistsError creates a tenant already exists error
func NewTenantAlreadyExistsError() *AppError {
return &AppError{
Code: ErrTenantAlreadyExists,
Message: "租户已存在",
HTTPCode: http.StatusConflict,
}
}
// NewTenantInactiveError creates a tenant inactive error
func NewTenantInactiveError() *AppError {
return &AppError{
Code: ErrTenantInactive,
Message: "租户已停用",
HTTPCode: http.StatusForbidden,
}
}
// Agent related errors
func NewAgentMissingThinkingModelError() *AppError {
return &AppError{
Code: ErrAgentMissingThinkingModel,
Message: "启用Agent模式前,请先选择思考模型",
HTTPCode: http.StatusBadRequest,
}
}
func NewAgentMissingAllowedToolsError() *AppError {
return &AppError{
Code: ErrAgentMissingAllowedTools,
Message: "至少需要选择一个允许的工具",
HTTPCode: http.StatusBadRequest,
}
}
func NewAgentInvalidMaxIterationsError() *AppError {
return &AppError{
Code: ErrAgentInvalidMaxIterations,
Message: "最大迭代次数必须在1-20之间",
HTTPCode: http.StatusBadRequest,
}
}
func NewAgentInvalidTemperatureError() *AppError {
return &AppError{
Code: ErrAgentInvalidTemperature,
Message: "温度参数必须在0-2之间",
HTTPCode: http.StatusBadRequest,
}
}
// NewVectorStoreBindingInvalidError signals that a knowledge base create
// request referenced a vector store that does not exist under the caller's
// tenant (or carried a malformed UUID). The user-facing message is
// intentionally generic to avoid enumeration oracles — see the structured
// log at the call site for the tenant/store pair.
func NewVectorStoreBindingInvalidError(message string) *AppError {
if message == "" {
message = "vector store not found"
}
return &AppError{
Code: ErrVectorStoreBindingInvalid,
Message: message,
HTTPCode: http.StatusBadRequest,
}
}
// NewVectorStoreUnavailableError signals that the requested vector store
// row exists in the database but is not currently wired into the in-memory
// engine registry (factory failure on CreateStore, dynamic config error,
// or stale state after a connection-config rotation).
func NewVectorStoreUnavailableError(message string) *AppError {
if message == "" {
message = "vector store is currently unavailable"
}
return &AppError{
Code: ErrVectorStoreUnavailable,
Message: message,
HTTPCode: http.StatusBadRequest,
}
}
// IsAppError checks if the error is an AppError type
func IsAppError(err error) (*AppError, bool) {
appErr, ok := err.(*AppError)
return appErr, ok
}