Files
story/internal/httpapi/ops_access_handlers.go
T

170 lines
5.6 KiB
Go
Raw Normal View History

2026-08-26 06:41:07 +08:00
package httpapi
import (
"errors"
"net/http"
"strings"
"time"
"data-terminal-cloud/internal/idempotency"
"data-terminal-cloud/internal/opsaccess"
)
func CreateOpsSensitiveAccessRequestHandler(service *opsaccess.Service) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
principal, ok := webPrincipal(w, r)
if !ok {
return
}
tenantID, key, ok := opsCreateHeaders(w, r)
if !ok {
return
}
var input struct {
ConversationID string `json:"conversation_id"`
Capabilities []string `json:"capabilities"`
Reason string `json:"reason"`
ExpiresAt time.Time `json:"expires_at"`
}
if !decodeWebJSON(w, r, &input, "临时敏感数据访问申请参数不正确") {
return
}
item, created, err := service.CreateRequest(r.Context(), principal, tenantID, input.ConversationID, input.Capabilities, input.Reason, input.ExpiresAt, key, RequestID(r.Context()))
if err != nil {
writeOpsAccessError(w, r, err)
return
}
status := http.StatusCreated
if !created {
status = http.StatusOK
w.Header().Set("Idempotency-Replayed", "true")
}
w.Header().Set("Cache-Control", "no-store")
WriteJSON(w, r, status, item)
})
}
func ListOpsSensitiveAccessRequestsHandler(service *opsaccess.Service) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
principal, ok := webPrincipal(w, r)
if !ok {
return
}
tenantID, ok := opsTenantHeader(w, r)
if !ok {
return
}
items, err := service.ListRequests(r.Context(), principal, tenantID)
if err != nil {
writeOpsAccessError(w, r, err)
return
}
w.Header().Set("Cache-Control", "no-store")
WriteJSON(w, r, http.StatusOK, map[string]any{"items": items})
})
}
func ReviewOpsSensitiveAccessRequestHandler(service *opsaccess.Service) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
principal, ok := webPrincipal(w, r)
if !ok {
return
}
tenantID, key, ok := opsCreateHeaders(w, r)
if !ok {
return
}
var input struct {
Decision string `json:"decision"`
Reason string `json:"reason"`
}
if !decodeWebJSON(w, r, &input, "临时访问审批参数不正确") {
return
}
item, created, err := service.ReviewRequest(r.Context(), principal, tenantID, r.PathValue("id"), input.Decision, input.Reason, key, RequestID(r.Context()))
if err != nil {
writeOpsAccessError(w, r, err)
return
}
if !created {
w.Header().Set("Idempotency-Replayed", "true")
}
w.Header().Set("Cache-Control", "no-store")
WriteJSON(w, r, http.StatusOK, item)
})
}
func GetOpsSensitiveConversationHandler(service *opsaccess.Service) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
principal, ok := webPrincipal(w, r)
if !ok {
return
}
tenantID, ok := opsTenantHeader(w, r)
if !ok {
return
}
grantID := strings.TrimSpace(r.Header.Get("X-Ops-Access-Grant"))
if grantID == "" {
writeValidation(w, r, "临时访问凭据缺失", FieldError{Field: "X-Ops-Access-Grant", Message: "必须提供已批准且未过期的临时访问申请编号"})
return
}
item, err := service.GetSensitiveConversation(r.Context(), principal, tenantID, r.PathValue("id"), grantID, RequestID(r.Context()))
if err != nil {
writeOpsAccessError(w, r, err)
return
}
w.Header().Set("Cache-Control", "no-store")
WriteJSON(w, r, http.StatusOK, item)
})
}
func CreateOpsSensitiveRecordingAccessHandler(service *opsaccess.Service) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
principal, ok := webPrincipal(w, r)
if !ok {
return
}
tenantID, ok := opsTenantHeader(w, r)
if !ok {
return
}
var input struct {
GrantID string `json:"grant_id"`
}
if !decodeWebJSON(w, r, &input, "录音临时访问参数不正确") {
return
}
if strings.TrimSpace(input.GrantID) == "" {
writeValidation(w, r, "录音临时访问参数不正确", FieldError{Field: "grant_id", Message: "必须提供已批准且未过期的临时访问申请编号"})
return
}
item, err := service.CreateRecordingAccess(r.Context(), principal, tenantID, r.PathValue("id"), input.GrantID, RequestID(r.Context()))
if err != nil {
writeOpsAccessError(w, r, err)
return
}
w.Header().Set("Cache-Control", "no-store")
WriteJSON(w, r, http.StatusCreated, item)
})
}
func writeOpsAccessError(w http.ResponseWriter, r *http.Request, err error) {
switch {
case errors.Is(err, opsaccess.ErrInvalid):
writeValidation(w, r, "临时敏感数据访问参数不正确", FieldError{Field: "temporary_access", Message: "通话、能力、原因或有效时间不符合要求"})
case errors.Is(err, opsaccess.ErrAccessDenied):
WriteError(w, r, http.StatusForbidden, "TEMPORARY_ACCESS_DENIED", "临时访问未批准、已过期、能力不匹配或不属于当前申请人")
case errors.Is(err, opsaccess.ErrNotFound):
WriteError(w, r, http.StatusNotFound, "RESOURCE_NOT_FOUND", "指定临时访问申请、通话或录音不存在")
case errors.Is(err, opsaccess.ErrConflict):
WriteError(w, r, http.StatusConflict, "TEMPORARY_ACCESS_STATE_CONFLICT", "已有未结束申请、申请人尝试自批或当前状态不允许操作")
case errors.Is(err, idempotency.ErrKeyConflict):
WriteError(w, r, http.StatusConflict, "IDEMPOTENCY_KEY_CONFLICT", "重复请求识别码已被不同请求使用")
case errors.Is(err, idempotency.ErrInProgress):
WriteError(w, r, http.StatusConflict, "IDEMPOTENCY_IN_PROGRESS", "相同请求正在处理中")
default:
WriteError(w, r, http.StatusInternalServerError, "INTERNAL_ERROR", "临时敏感数据访问处理失败")
}
}