发布 0.1.0:通知渠道、告警规则、令牌版本与安全加固
This commit is contained in:
+65
-50
@@ -2,17 +2,19 @@ package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"oci-portal/internal/oci"
|
||||
)
|
||||
|
||||
// 审计查询时间窗(小时)上下限。
|
||||
// 批式懒加载的单批条数上下限与缺省值。
|
||||
const (
|
||||
minAuditHours = 1
|
||||
maxAuditHours = 72
|
||||
auditLimitDefault = 100
|
||||
auditLimitMax = 200
|
||||
)
|
||||
|
||||
// 审计原始事件缓存:约 3KB/条,上限 2000 条 ≈ 6MB;TTL 内详情秒开。
|
||||
@@ -21,39 +23,32 @@ const (
|
||||
auditRawTTL = 10 * time.Minute
|
||||
)
|
||||
|
||||
// ErrInvalidAuditHours 表示审计时间窗参数越界,handler 据此返回 400。
|
||||
var ErrInvalidAuditHours = errors.New("audit events: hours must be between 1 and 72")
|
||||
|
||||
// ErrInvalidAuditWindow 表示续查的绝对时间窗非法(格式/顺序/跨度),handler 返回 400。
|
||||
var ErrInvalidAuditWindow = errors.New("audit events: invalid start/end window")
|
||||
// ErrInvalidAuditCursor 表示续查游标不可解析(过期格式/被篡改),handler 返回 400。
|
||||
var ErrInvalidAuditCursor = errors.New("audit events: invalid cursor, refresh to restart")
|
||||
|
||||
// ErrAuditEventGone 表示原始事件缓存过期且小窗重查未找回;handler 映射 404。
|
||||
var ErrAuditEventGone = errors.New("原始事件已不可取回,请刷新列表后重试")
|
||||
|
||||
// AuditQuery 是审计查询参数:Hours 为首查(相对当前时刻);
|
||||
// Start/End(RFC3339)为续查绝对窗,配合 Page 从截断游标断点续翻。
|
||||
// AuditQuery 是批式懒加载查询参数:Cursor 为空表示自当前时刻首查,
|
||||
// 非空则从上次响应的游标位置继续向更早回溯;Limit 为单批目标条数。
|
||||
type AuditQuery struct {
|
||||
Region string
|
||||
Hours int
|
||||
Start string
|
||||
End string
|
||||
Page string
|
||||
Cursor string
|
||||
Limit int
|
||||
}
|
||||
|
||||
// AuditEventsView 是审计查询响应:列表不含 raw(详情接口取回);
|
||||
// Start/End 回传本次实际使用的绝对窗,截断续查必须原样带回(游标绑定查询参数)。
|
||||
// AuditEventsView 是批式查询响应:列表不含 raw(详情接口取回);
|
||||
// Cursor 供下一批续查原样带回,空且 Exhausted 表示已到 365 天保留期尽头。
|
||||
type AuditEventsView struct {
|
||||
Items []oci.AuditEvent `json:"items"`
|
||||
Truncated bool `json:"truncated"`
|
||||
NextPage string `json:"nextPage,omitempty"`
|
||||
Start time.Time `json:"start"`
|
||||
End time.Time `json:"end"`
|
||||
Cursor string `json:"cursor,omitempty"`
|
||||
Exhausted bool `json:"exhausted"`
|
||||
}
|
||||
|
||||
// AuditEvents 实时查询租户 OCI 审计事件,纯透传不入库;region 为空时用配置
|
||||
// 默认区域。原始事件剥离进进程内缓存,响应体瘦身约 10 倍(调研④方案 A1+B1)。
|
||||
func (s *OciConfigService) AuditEvents(ctx context.Context, id uint, q AuditQuery) (AuditEventsView, error) {
|
||||
start, end, err := auditWindow(q)
|
||||
cur, err := decodeAuditCursor(q.Cursor)
|
||||
if err != nil {
|
||||
return AuditEventsView{}, err
|
||||
}
|
||||
@@ -61,53 +56,73 @@ func (s *OciConfigService) AuditEvents(ctx context.Context, id uint, q AuditQuer
|
||||
if err != nil {
|
||||
return AuditEventsView{}, err
|
||||
}
|
||||
res, err := s.client.ListAuditEvents(ctx, cred, q.Region, start, end, q.Page)
|
||||
res, err := s.client.ListAuditEventsBatch(ctx, cred, q.Region, cur, auditLimit(q.Limit))
|
||||
if err != nil {
|
||||
return AuditEventsView{}, err
|
||||
}
|
||||
return AuditEventsView{
|
||||
Items: s.stripAuditRaw(res.Items),
|
||||
Truncated: res.Truncated,
|
||||
NextPage: res.NextPage,
|
||||
// 与实际请求同粒度(分钟),续查回传时窗口逐字节一致
|
||||
Start: start.UTC().Truncate(time.Minute),
|
||||
End: end.UTC().Truncate(time.Minute),
|
||||
}, nil
|
||||
view := AuditEventsView{Items: s.stripAuditRaw(id, res.Items), Exhausted: res.Exhausted}
|
||||
if res.Cursor != nil {
|
||||
view.Cursor = encodeAuditCursor(*res.Cursor)
|
||||
}
|
||||
return view, nil
|
||||
}
|
||||
|
||||
// auditWindow 解析查询窗口:带 start/end/page 走绝对窗校验,否则按 hours 相对窗。
|
||||
func auditWindow(q AuditQuery) (time.Time, time.Time, error) {
|
||||
if q.Start != "" || q.End != "" || q.Page != "" {
|
||||
start, err1 := time.Parse(time.RFC3339, q.Start)
|
||||
end, err2 := time.Parse(time.RFC3339, q.End)
|
||||
if err1 != nil || err2 != nil || !start.Before(end) ||
|
||||
end.Sub(start) > maxAuditHours*time.Hour+time.Minute {
|
||||
return time.Time{}, time.Time{}, ErrInvalidAuditWindow
|
||||
}
|
||||
return start, end, nil
|
||||
// auditLimit 归一单批条数:缺省 100,上限 200(响应体量与页预算的平衡)。
|
||||
func auditLimit(limit int) int {
|
||||
if limit <= 0 {
|
||||
return auditLimitDefault
|
||||
}
|
||||
if q.Hours < minAuditHours || q.Hours > maxAuditHours {
|
||||
return time.Time{}, time.Time{}, ErrInvalidAuditHours
|
||||
}
|
||||
end := time.Now().UTC()
|
||||
return end.Add(-time.Duration(q.Hours) * time.Hour), end, nil
|
||||
return min(limit, auditLimitMax)
|
||||
}
|
||||
|
||||
// stripAuditRaw 把每条原始事件按 eventId 放进缓存并从列表剥离。
|
||||
func (s *OciConfigService) stripAuditRaw(items []oci.AuditEvent) []oci.AuditEvent {
|
||||
// encodeAuditCursor 把游标序列化为不透明字符串(base64url JSON)。
|
||||
// 内容仅时间窗与 OCI 翻页令牌,伪造只影响自己的查询范围,无需签名。
|
||||
func encodeAuditCursor(cur oci.AuditCursor) string {
|
||||
b, _ := json.Marshal(cur)
|
||||
return base64.RawURLEncoding.EncodeToString(b)
|
||||
}
|
||||
|
||||
// decodeAuditCursor 解析续查游标;空串返回自当前时刻起的首查游标。
|
||||
func decodeAuditCursor(s string) (oci.AuditCursor, error) {
|
||||
if s == "" {
|
||||
return oci.NewAuditCursor(time.Now()), nil
|
||||
}
|
||||
b, err := base64.RawURLEncoding.DecodeString(s)
|
||||
if err != nil {
|
||||
return oci.AuditCursor{}, ErrInvalidAuditCursor
|
||||
}
|
||||
var cur oci.AuditCursor
|
||||
if err := json.Unmarshal(b, &cur); err != nil || cur.Start.IsZero() || !cur.Start.Before(cur.End) {
|
||||
return oci.AuditCursor{}, ErrInvalidAuditCursor
|
||||
}
|
||||
return cur, nil
|
||||
}
|
||||
|
||||
// auditRawKey 组装租户隔离的原始事件缓存键。
|
||||
func auditRawKey(configID uint, eventID string) string {
|
||||
return strconv.FormatUint(uint64(configID), 10) + ":" + eventID
|
||||
}
|
||||
|
||||
// stripAuditRaw 把每条原始事件按租户与 eventId 放进缓存并从列表剥离。
|
||||
func (s *OciConfigService) stripAuditRaw(configID uint, items []oci.AuditEvent) []oci.AuditEvent {
|
||||
for i := range items {
|
||||
if items[i].EventId != "" && items[i].Raw != nil {
|
||||
s.auditRaw.Set(items[i].EventId, items[i].Raw, auditRawTTL)
|
||||
s.auditRaw.Set(auditRawKey(configID, items[i].EventId), items[i].Raw, auditRawTTL)
|
||||
}
|
||||
items[i].Raw = nil
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
// InvalidateAuditCache 删除指定租户的全部审计原始事件缓存。
|
||||
func (s *OciConfigService) InvalidateAuditCache(configID uint) {
|
||||
s.auditRaw.DeletePrefix(strconv.FormatUint(uint64(configID), 10) + ":")
|
||||
}
|
||||
|
||||
// AuditEventDetail 取回单条事件的原始 JSON:缓存命中即回;miss 按事件时间
|
||||
// 所在分钟起 2 分钟小窗重查兜底(调研④方案 A2),仍未命中报 ErrAuditEventGone。
|
||||
func (s *OciConfigService) AuditEventDetail(ctx context.Context, id uint, region, eventID string, eventTime time.Time) (json.RawMessage, error) {
|
||||
if raw, ok := s.auditRaw.Get(eventID); ok {
|
||||
if raw, ok := s.auditRaw.Get(auditRawKey(id, eventID)); ok {
|
||||
return raw.(json.RawMessage), nil
|
||||
}
|
||||
cred, err := s.credentialsByID(ctx, id)
|
||||
@@ -124,7 +139,7 @@ func (s *OciConfigService) AuditEventDetail(ctx context.Context, id uint, region
|
||||
if ev.EventId == "" || ev.Raw == nil {
|
||||
continue
|
||||
}
|
||||
s.auditRaw.Set(ev.EventId, ev.Raw, auditRawTTL)
|
||||
s.auditRaw.Set(auditRawKey(id, ev.EventId), ev.Raw, auditRawTTL)
|
||||
if ev.EventId == eventID {
|
||||
found = ev.Raw
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user