0.11.6: 渠道权限管控(部门范围 + 用户级授权)

- 迁移 000047:channels.department_ids(空=全局) + channel_grants 用户级授权
  (source=manual/approval 区分来源)。
- 管理端:渠道部门范围配置 + 授权管理(列表/授予/撤销);渠道列表显示范围。
- 门户:我的渠道端点(/api/v1/portal/channels)按部门可见或明确授权返回,
  「个人渠道」页新增可使用渠道区(授权方式标识)。
- 审批流:资源申请中的渠道类型通过后自动写 channel_grants(source=approval),
  取代'批准记录即授权'的弱语义。
- 端到端验证:部门隔离(demo 无部门看不到)→手动授予→可见→撤销→不可见;
  审批通过自动授权。修复 JOIN 列歧义与 uuid/text 比较。
This commit is contained in:
LLMGuardX Dev
2026-08-13 15:03:39 +08:00
parent 277f80dc82
commit 8000bccde3
10 changed files with 392 additions and 26 deletions
+30
View File
@@ -63,6 +63,8 @@ func NewHTTPHandler(service *Service, identityService *identity.Service) *HTTPHa
h.mux.HandleFunc("POST /api/v1/portal/personal-channels", h.createPersonalChannel)
h.mux.HandleFunc("POST /api/v1/portal/personal-channels/{id}/token", h.regeneratePersonalToken)
h.mux.HandleFunc("DELETE /api/v1/portal/personal-channels/{id}", h.deletePersonalChannel)
// 我的渠道:部门可见或已授权。
h.mux.HandleFunc("GET /api/v1/portal/channels", h.myChannels)
// 数字员工:会话入口 + 调用记录。
h.mux.HandleFunc("GET /api/v1/portal/digital-employees", h.digitalEmployees)
h.mux.HandleFunc("POST /api/v1/portal/digital-employees/{code}/chat", h.runDigitalEmployee)
@@ -925,3 +927,31 @@ func (h *HTTPHandler) myEmployeeRuns(w http.ResponseWriter, r *http.Request) {
}
apiresponse.OK(w, items)
}
// --- 我的渠道:部门可见或已授权 ---
func (h *HTTPHandler) myChannels(w http.ResponseWriter, r *http.Request) {
a, ok := h.account(w, r)
if !ok {
return
}
if h.service.channels == nil {
apiresponse.OK(w, map[string]any{"channels": []any{}, "granted_codes": []string{}})
return
}
visible, err := h.service.channels.VisibleChannelsForUser(r.Context(), a.ID, a.DepartmentID)
if err != nil {
portalError(w, err)
return
}
granted, err := h.service.channels.GrantsForUser(r.Context(), a.ID)
if err != nil {
portalError(w, err)
return
}
grantedCodes := make([]string, 0, len(granted))
for _, item := range granted {
grantedCodes = append(grantedCodes, item.Code)
}
apiresponse.OK(w, map[string]any{"channels": visible, "granted_codes": grantedCodes})
}
+20 -4
View File
@@ -180,10 +180,26 @@ func (s *Service) DecideResourceRequest(ctx context.Context, id, status, note, a
if err = tx.QueryRow(ctx, `SELECT portal_user_id::text,resource_type,resource_code FROM gateway.resource_access_requests WHERE id=$1`, id).Scan(&userID, &resourceType, &resourceCode); err != nil {
return ResourceRequest{}, err
}
if status == "approved" && resourceType != "channel" && s.market != nil {
// 自动安装到申请用户工作区(use 等级)。
if _, err = s.market.Install(ctx, resourceType, resourceCode, userID, "use"); err != nil {
return ResourceRequest{}, err
if status == "approved" {
switch resourceType {
case "mcp_server", "skill", "digital_employee":
if s.market != nil {
// 自动安装到申请用户工作区(use 等级)。
if _, err = s.market.Install(ctx, resourceType, resourceCode, userID, "use"); err != nil {
return ResourceRequest{}, err
}
}
case "channel":
// 渠道审批通过 = 写入 channel_grants 用户级授权。
if s.channels != nil {
var channelID string
if err = tx.QueryRow(ctx, `SELECT id::text FROM gateway.channels WHERE code=$1`, resourceCode).Scan(&channelID); err != nil {
return ResourceRequest{}, err
}
if err = s.channels.Grant(ctx, channelID, userID, actorID, "approval"); err != nil {
return ResourceRequest{}, err
}
}
}
}
payload, _ := json.Marshal(map[string]any{"request_id": id, "portal_user_id": userID, "resource_type": resourceType, "resource_code": resourceCode, "status": status, "actor_id": actorID})
+6
View File
@@ -9,6 +9,7 @@ import (
"strings"
"time"
"aigateway.local/core/internal/channel"
"aigateway.local/core/internal/identity"
platformid "aigateway.local/core/internal/platform/id"
"aigateway.local/core/internal/workbench"
@@ -28,6 +29,7 @@ type Service struct {
runtime http.Handler
gateway http.Handler
market *workbench.MarketplaceService
channels *channel.Service
}
func NewService(pool *pgxpool.Pool, assets *workbench.Service, tools *workbench.ToolService, identityService *identity.Service) *Service {
@@ -46,6 +48,10 @@ func (s *Service) SetApplicationRuntime(credentials *RuntimeCredentials, runtime
// /v1/chat/completions with the user's own runtime credential.
func (s *Service) SetGateway(gateway http.Handler) { s.gateway = gateway }
// SetChannelService wires the channel service for approval auto-grant and the
// portal "my channels" visibility endpoint.
func (s *Service) SetChannelService(service *channel.Service) { s.channels = service }
// SetMarketplace wires the resource-marketplace service into the portal so the
// marketplace pages can browse, install and manage resources.
func (s *Service) SetMarketplace(market *workbench.MarketplaceService) {