mirror of
https://github.com/Tencent/WeKnora.git
synced 2026-06-04 13:30:32 +08:00
Tenant RBAC headline release: 4-tier role matrix (Owner/Admin/ Contributor/Viewer), per-KB resource ownership, per-tenant audit log, tenant member management, self-service workspaces. Also: CLI v0.3/v0.4 GA, KB retrieval fan-out across vector stores, AES-256-GCM credential at-rest, docreader gRPC TLS+Token, Zhipu embedding, Huawei OBS, vLLM URL for MinerU, Apache Doris compat modes, server-side user preferences, Go 1.26.0. See CHANGELOG.md for the full list. docs(rbac): wire RBAC screenshots into READMEs and RBAC guide - README.md / README_CN.md / README_JA.md / README_KO.md: replace the single member-management thumbnail under the v0.6.0 RBAC highlight with a 2×2 showcase (member management, workspace switcher, self-service workspace creation, pending invitations). - docs/RBAC说明.md: add the member-management screenshot to the existing 前端实际界面 showcase so the guide is self-contained and no longer cross-references README for it. feat(rbac-ui): link tenant member page to RBAC guide Add an inline doc-link in the Tenant Members settings page that opens docs/RBAC说明.md on GitHub in a new tab, complementing the existing in-app role-matrix popover. New i18n key tenantMember.learnRbacGuide covered for zh-CN / en-US / ko-KR / ru-RU.
107 lines
3.7 KiB
Go
107 lines
3.7 KiB
Go
package handler
|
||
|
||
import (
|
||
"net/http"
|
||
"strconv"
|
||
|
||
"github.com/Tencent/WeKnora/internal/errors"
|
||
"github.com/Tencent/WeKnora/internal/logger"
|
||
"github.com/Tencent/WeKnora/internal/types"
|
||
"github.com/Tencent/WeKnora/internal/types/interfaces"
|
||
"github.com/gin-gonic/gin"
|
||
)
|
||
|
||
// AuditLogHandler exposes the per-tenant audit-log feed (PR 6, #1303).
|
||
// The route group lives under /tenants/:id/audit-log, gated by
|
||
// PathTenantMatch (URL :id == active tenant) plus an Admin role
|
||
// requirement — leaks of denied-action histories should not surface
|
||
// to ordinary members.
|
||
type AuditLogHandler struct {
|
||
auditService interfaces.AuditLogService
|
||
}
|
||
|
||
// NewAuditLogHandler constructs the handler.
|
||
func NewAuditLogHandler(auditService interfaces.AuditLogService) *AuditLogHandler {
|
||
return &AuditLogHandler{auditService: auditService}
|
||
}
|
||
|
||
// auditLogListResponse is the response envelope for ListTenantAuditLog.
|
||
// Mirrors wiki_log_entries' shape: data array + an opaque cursor (here
|
||
// the integer id of the last entry, or 0 if no more rows).
|
||
type auditLogListResponse struct {
|
||
Success bool `json:"success"`
|
||
Data []*types.AuditLog `json:"data"`
|
||
NextCursor uint64 `json:"next_cursor"`
|
||
}
|
||
|
||
// ListTenantAuditLog godoc
|
||
// @Summary 获取租户审计日志
|
||
// @Description 返回该租户最近的审计事件,按 id 倒序。游标分页:将上次响应的 next_cursor 作为下一次请求的 after_id。
|
||
// @Tags 审计日志
|
||
// @Produce json
|
||
// @Param id path string true "租户ID"
|
||
// @Param after_id query int false "游标:返回 id 小于此值的记录(默认从最新开始)"
|
||
// @Param limit query int false "页大小,1-100,默认 50"
|
||
// @Param action query string false "按 action 精确过滤(如 rbac.member_added / rbac.access_denied)"
|
||
// @Param outcome query string false "按 outcome 精确过滤(success / denied)"
|
||
// @Param actor query string false "按 actor_user_id 精确过滤"
|
||
// @Success 200 {object} auditLogListResponse
|
||
// @Failure 400 {object} errors.AppError
|
||
// @Security Bearer
|
||
// @Security ApiKeyAuth
|
||
// @Router /tenants/{id}/audit-log [get]
|
||
func (h *AuditLogHandler) ListTenantAuditLog(c *gin.Context) {
|
||
ctx := c.Request.Context()
|
||
tenantID, ok := parseTenantIDFromPath(c)
|
||
if !ok {
|
||
// parseTenantIDFromPath has already attached an error to gin.
|
||
return
|
||
}
|
||
|
||
// after_id cursor — invalid values are tolerated (treated as "from
|
||
// the top") so a misconfigured client doesn't see a hard 400 on
|
||
// the empty / first request. Tighter validation belongs at the
|
||
// frontend.
|
||
var afterID uint64
|
||
if raw := c.Query("after_id"); raw != "" {
|
||
if v, err := strconv.ParseUint(raw, 10, 64); err == nil {
|
||
afterID = v
|
||
}
|
||
}
|
||
limit := 0 // 0 lets the repository pick its default (50)
|
||
if raw := c.Query("limit"); raw != "" {
|
||
if v, err := strconv.Atoi(raw); err == nil && v > 0 {
|
||
limit = v
|
||
}
|
||
}
|
||
|
||
q := &interfaces.AuditLogQuery{
|
||
AfterID: afterID,
|
||
Limit: limit,
|
||
Action: types.AuditAction(c.Query("action")),
|
||
Outcome: types.AuditOutcome(c.Query("outcome")),
|
||
ActorUserID: c.Query("actor"),
|
||
}
|
||
|
||
entries, err := h.auditService.List(ctx, tenantID, q)
|
||
if err != nil {
|
||
logger.ErrorWithFields(ctx, err, map[string]interface{}{"tenant_id": tenantID})
|
||
c.Error(errors.NewInternalServerError(err.Error()))
|
||
return
|
||
}
|
||
|
||
// next_cursor is the smallest id in the page (since rows are sorted
|
||
// id DESC). Empty page ⇒ 0, telling the client there's nothing
|
||
// older to fetch.
|
||
var nextCursor uint64
|
||
if n := len(entries); n > 0 {
|
||
nextCursor = entries[n-1].ID
|
||
}
|
||
|
||
c.JSON(http.StatusOK, auditLogListResponse{
|
||
Success: true,
|
||
Data: entries,
|
||
NextCursor: nextCursor,
|
||
})
|
||
}
|