feat: add audit logs

This commit is contained in:
root 2024-04-12 06:31:13 +00:00
parent 50c3539cb0
commit 10c317f07c
16 changed files with 1157 additions and 65 deletions

29
common/audit_log.go Normal file
View File

@ -0,0 +1,29 @@
/* Copyright © INFINI Ltd. All rights reserved.
* Web: https://infinilabs.com
* Email: hello#infini.ltd */
package common
import (
"infini.sh/framework/core/api"
"net/http"
"strings"
)
// GetClientIP 获取客户端 IP 地址
func GetClientIP(req *http.Request) string {
h := new(api.Handler)
// 1. 尝试从 XFF 头获取
xff := h.GetHeader(req, "X-Forwarded-For", "")
clientIP := strings.Split(xff, ",")[0]
if len(clientIP) > 0 {
return clientIP
}
// 2. 尝试从 X-Real-IP 头获取
clientIP = h.GetHeader(req, "X-Real-IP", "")
if len(clientIP) > 0 {
return clientIP
}
// 3. 从 RemoteAddr 获取
return strings.Split(req.RemoteAddr, ":")[0]
}

View File

@ -612,4 +612,109 @@ PUT $[[SETUP_INDEX_PREFIX]]activities-00001
"is_write_index": true
}
}
}
PUT _template/$[[SETUP_INDEX_PREFIX]]audit-logs-rollover
{
"order" : 100000,
"index_patterns" : [
"$[[SETUP_INDEX_PREFIX]]audit-logs*"
],
"settings" : {
"index" : {
"format" : "7",
"lifecycle" : {
"name" : "ilm_$[[SETUP_INDEX_PREFIX]]metrics-30days-retention",
"rollover_alias" : "$[[SETUP_INDEX_PREFIX]]audit-logs"
},
"codec" : "best_compression",
"number_of_shards" : "1",
"translog.durability":"async"
}
},
"mappings" : {
"dynamic_templates" : [
{
"strings" : {
"mapping" : {
"ignore_above" : 256,
"type" : "keyword"
},
"match_mapping_type" : "string"
}
}
]
},
"aliases" : { }
}
PUT $[[SETUP_INDEX_PREFIX]]audit-logs-00001
{
"mappings": {
"dynamic_templates": [
{
"strings": {
"match_mapping_type": "string",
"mapping": {
"ignore_above": 256,
"type": "keyword"
}
}
}
],
"properties": {
"id": {
"type": "keyword"
},
"metadata": {
"properties": {
"operator": {
"type": "keyword",
"ignore_above": 256
},
"log_type": {
"type": "keyword",
"ignore_above": 256
},
"resource_type": {
"type": "keyword",
"ignore_above": 256
}
}
},
"timestamp": {
"type": "date"
}
}
},
"settings": {
"index": {
"lifecycle.rollover_alias": "$[[SETUP_INDEX_PREFIX]]audit-logs",
"refresh_interval": "5s",
"mapping": {
"total_fields": {
"limit": "20000"
}
},
"max_result_window": "10000000",
"analysis": {
"analyzer": {
"suggest_text_search": {
"filter": [
"lowercase",
"word_delimiter"
],
"tokenizer": "classic"
}
}
}
}
},
"aliases": {
"$[[SETUP_INDEX_PREFIX]]audit-logs": {
"is_write_index": true
}
}
}

View File

@ -612,4 +612,109 @@ PUT $[[SETUP_INDEX_PREFIX]]activities-00001
"is_write_index": true
}
}
}
PUT _template/$[[SETUP_INDEX_PREFIX]]audit-logs-rollover
{
"order" : 100000,
"index_patterns" : [
"$[[SETUP_INDEX_PREFIX]]audit-logs*"
],
"settings" : {
"index" : {
"format" : "7",
"lifecycle" : {
"name" : "ilm_$[[SETUP_INDEX_PREFIX]]metrics-30days-retention",
"rollover_alias" : "$[[SETUP_INDEX_PREFIX]]audit-logs"
},
"codec" : "best_compression",
"number_of_shards" : "1",
"translog.durability":"async"
}
},
"mappings" : {
"dynamic_templates" : [
{
"strings" : {
"mapping" : {
"ignore_above" : 256,
"type" : "keyword"
},
"match_mapping_type" : "string"
}
}
]
},
"aliases" : { }
}
PUT $[[SETUP_INDEX_PREFIX]]audit-logs-00001
{
"mappings": {
"dynamic_templates": [
{
"strings": {
"match_mapping_type": "string",
"mapping": {
"ignore_above": 256,
"type": "keyword"
}
}
}
],
"properties": {
"id": {
"type": "keyword"
},
"metadata": {
"properties": {
"operator": {
"type": "keyword",
"ignore_above": 256
},
"log_type": {
"type": "keyword",
"ignore_above": 256
},
"resource_type": {
"type": "keyword",
"ignore_above": 256
}
}
},
"timestamp": {
"type": "date"
}
}
},
"settings": {
"index": {
"lifecycle.rollover_alias": "$[[SETUP_INDEX_PREFIX]]audit-logs",
"refresh_interval": "5s",
"mapping": {
"total_fields": {
"limit": "20000"
}
},
"max_result_window": "10000000",
"analysis": {
"analyzer": {
"suggest_text_search": {
"filter": [
"lowercase",
"word_delimiter"
],
"tokenizer": "classic"
}
}
}
}
},
"aliases": {
"$[[SETUP_INDEX_PREFIX]]audit-logs": {
"is_write_index": true
}
}
}

View File

@ -553,4 +553,106 @@ PUT $[[SETUP_INDEX_PREFIX]]activities-00001
"is_write_index": true
}
}
}
PUT _template/$[[SETUP_INDEX_PREFIX]]audit-logs-rollover
{
"order" : 100000,
"template" : "$[[SETUP_INDEX_PREFIX]]audit-logs*",
"settings" : {
"index" : {
"format" : "7",
"codec" : "best_compression",
"number_of_shards" : "1",
"translog.durability":"async"
}
},
"mappings" : {
"doc":{
"dynamic_templates" : [
{
"strings" : {
"mapping" : {
"ignore_above" : 256,
"type" : "keyword"
},
"match_mapping_type" : "string"
}
}
]
}
},
"aliases" : { }
}
PUT $[[SETUP_INDEX_PREFIX]]audit-logs-00001
{
"mappings": {
"doc":{
"dynamic_templates": [
{
"strings": {
"match_mapping_type": "string",
"mapping": {
"ignore_above": 256,
"type": "keyword"
}
}
}
],
"properties": {
"id": {
"type": "keyword"
},
"metadata": {
"properties": {
"operator": {
"type": "keyword",
"ignore_above": 256
},
"log_type": {
"type": "keyword",
"ignore_above": 256
},
"resource_type": {
"type": "keyword",
"ignore_above": 256
}
}
},
"timestamp": {
"type": "date"
}
}
}
},
"settings": {
"index": {
"refresh_interval": "5s",
"mapping": {
"total_fields": {
"limit": "20000"
}
},
"max_result_window": "10000000",
"analysis": {
"analyzer": {
"suggest_text_search": {
"filter": [
"lowercase",
"word_delimiter"
],
"tokenizer": "classic"
}
}
}
}
},
"aliases": {
"$[[SETUP_INDEX_PREFIX]]audit-logs": {
"is_write_index": true
}
}
}

View File

@ -630,4 +630,113 @@ PUT $[[SETUP_INDEX_PREFIX]]activities-00001
"is_write_index": true
}
}
}
PUT _template/$[[SETUP_INDEX_PREFIX]]audit-logs-rollover
{
"order" : 100000,
"index_patterns" : [
"$[[SETUP_INDEX_PREFIX]]audit-logs*"
],
"settings" : {
"index" : {
"format" : "7",
"lifecycle" : {
"name" : "ilm_$[[SETUP_INDEX_PREFIX]]metrics-30days-retention",
"rollover_alias" : "$[[SETUP_INDEX_PREFIX]]audit-logs"
},
"codec" : "best_compression",
"number_of_shards" : "1",
"translog.durability":"async"
}
},
"mappings" : {
"doc":{
"dynamic_templates" : [
{
"strings" : {
"mapping" : {
"ignore_above" : 256,
"type" : "keyword"
},
"match_mapping_type" : "string"
}
}
]
}
},
"aliases" : { }
}
PUT $[[SETUP_INDEX_PREFIX]]audit-logs-00001
{
"mappings": {
"doc":{
"dynamic_templates": [
{
"strings": {
"match_mapping_type": "string",
"mapping": {
"ignore_above": 256,
"type": "keyword"
}
}
}
],
"properties": {
"id": {
"type": "keyword"
},
"metadata": {
"properties": {
"operator": {
"type": "keyword",
"ignore_above": 256
},
"log_type": {
"type": "keyword",
"ignore_above": 256
},
"resource_type": {
"type": "keyword",
"ignore_above": 256
}
}
},
"timestamp": {
"type": "date"
}
}
}
},
"settings": {
"index": {
"lifecycle.rollover_alias": "$[[SETUP_INDEX_PREFIX]]audit-logs",
"refresh_interval": "5s",
"mapping": {
"total_fields": {
"limit": "20000"
}
},
"max_result_window": "10000000",
"analysis": {
"analyzer": {
"suggest_text_search": {
"filter": [
"lowercase",
"word_delimiter"
],
"tokenizer": "classic"
}
}
}
}
},
"aliases": {
"$[[SETUP_INDEX_PREFIX]]audit-logs": {
"is_write_index": true
}
}
}

View File

@ -597,6 +597,111 @@ PUT $[[SETUP_INDEX_PREFIX]]activities-00001
}
POST _plugins/_ism/add/$[[SETUP_INDEX_PREFIX]]activities-00001
{
"policy_id": "ilm_$[[SETUP_INDEX_PREFIX]]metrics-30days-retention"
}
PUT /_template/$[[SETUP_INDEX_PREFIX]]audit-logs-rollover
{
"order": 100000,
"index_patterns" : [
"$[[SETUP_INDEX_PREFIX]]audit-logs*"
],
"settings": {
"plugins.index_state_management.rollover_alias": "$[[SETUP_INDEX_PREFIX]]audit-logs",
"codec": "best_compression",
"number_of_shards": "1",
"translog": {
"durability": "async"
}
},
"mappings" : {
"dynamic_templates" : [
{
"strings" : {
"mapping" : {
"ignore_above" : 256,
"type" : "keyword"
},
"match_mapping_type" : "string"
}
}
]
},
"aliases" : { }
}
PUT $[[SETUP_INDEX_PREFIX]]audit-logs-00001
{
"mappings": {
"dynamic_templates": [
{
"strings": {
"match_mapping_type": "string",
"mapping": {
"ignore_above": 256,
"type": "keyword"
}
}
}
],
"properties": {
"id": {
"type": "keyword"
},
"metadata": {
"properties": {
"operator": {
"type": "keyword",
"ignore_above": 256
},
"log_type": {
"type": "keyword",
"ignore_above": 256
},
"resource_type": {
"type": "keyword",
"ignore_above": 256
}
}
},
"timestamp": {
"type": "date"
}
}
},
"settings": {
"index": {
"refresh_interval": "5s",
"mapping": {
"total_fields": {
"limit": "20000"
}
},
"max_result_window": "10000000",
"analysis": {
"analyzer": {
"suggest_text_search": {
"filter": [
"lowercase",
"word_delimiter"
],
"tokenizer": "classic"
}
}
}
}
},
"aliases": {
"$[[SETUP_INDEX_PREFIX]]audit-logs": {
"is_write_index": true
}
}
}
POST _plugins/_ism/add/$[[SETUP_INDEX_PREFIX]]audit-logs-00001
{
"policy_id": "ilm_$[[SETUP_INDEX_PREFIX]]metrics-30days-retention"
}

View File

@ -92,6 +92,22 @@ pipeline:
worker_size: 1
bulk_size_in_kb: 1
- name: merge_audit_log
auto_start: true
keep_running: true
processor:
- indexing_merge:
input_queue: "logging-audit-log-queue"
idle_timeout_in_seconds: 1
elasticsearch: "$[[CLUSTER_ID]]"
index_name: "$[[INDEX_PREFIX]]audit-logs"
output_queue:
name: "pipeline-audit-logs"
label:
tag: "audit_log_logging"
worker_size: 1
bulk_size_in_kb: 1
- name: ingest_merged_requests
auto_start: true
keep_running: true

View File

@ -6,6 +6,8 @@ import (
_ "expvar"
api3 "infini.sh/console/modules/agent/api"
"infini.sh/console/plugin/api/email"
"infini.sh/console/plugin/audit_log"
"infini.sh/framework/core/api"
model2 "infini.sh/framework/core/model"
_ "time/tzdata"
@ -59,7 +61,6 @@ func main() {
app.Init(nil)
defer app.Shutdown()
modules := []module.ModuleItem{}
modules = append(modules, module.ModuleItem{Value: &stats.SimpleStatsModule{}, Priority: 1})
modules = append(modules, module.ModuleItem{Value: &elastic2.ElasticModule{}, Priority: 1})
@ -90,7 +91,6 @@ func main() {
}
}
api3.Init()
appConfig = &config.AppConfig{
@ -113,6 +113,8 @@ func main() {
appUI = &UI{Config: appConfig}
appUI.InitUI()
api.AddGlobalInterceptors(new(audit_log.MonitoringInterceptor))
}, func() {
module.Start()
@ -138,7 +140,7 @@ func main() {
orm.RegisterSchemaWithIndexName(model.EmailServer{}, "email-server")
orm.RegisterSchemaWithIndexName(model2.Instance{}, "instance")
orm.RegisterSchemaWithIndexName(api3.RemoteConfig{}, "configs")
orm.RegisterSchemaWithIndexName(model.AuditLog{}, "audit-logs")
if global.Env().SetupRequired() {
for _, v := range modules {

369
model/audit_log.go Normal file
View File

@ -0,0 +1,369 @@
/* Copyright © INFINI Ltd. All rights reserved.
* Web: https://infinilabs.com
* Email: hello#infini.ltd */
package model
import (
"errors"
"fmt"
"infini.sh/framework/core/util"
"net"
"strings"
"time"
)
// 错误定义
var (
ErrCreatedTimeNotProvided = errors.New("created time not provided")
ErrOperatorNotProvided = errors.New("operator not provided")
ErrLogTypeNotProvided = errors.New("log type not provided or invalid")
ErrResourceTypeNotProvided = errors.New("resource type not provided or invalid")
ErrEventNotProvided = errors.New("event not provided")
ErrEventSourceIPNotProvided = errors.New("event source ip not provided or invalid")
ErrOperationNotProvided = errors.New("operation not provided or invalid")
)
const (
// LogTypeOperation 表示操作日志
LogTypeOperation = "operation"
// LogTypeAccess 表示访问日志
LogTypeAccess = "access"
// ResourceTypeAuditLog 表示审计日志
ResourceTypeAuditLog = "audit_log"
// ResourceTypeAccountCenter 表示账户中心
ResourceTypeAccountCenter = "account_center"
// ResourceTypeClusterManagement 表示集群管理
ResourceTypeClusterManagement = "cluster_management"
// ResourceTypeDevTool 表示开发工具
ResourceTypeDevTool = "dev_tool"
// ResourceTypeDataMigration 表示数据迁移
ResourceTypeDataMigration = "data_migration"
// OperationTypeLogin 表示登录
OperationTypeLogin = "login"
// OperationTypeDeletion 表示删除
OperationTypeDeletion = "deletion"
// OperationTypeModification 表示修改
OperationTypeModification = "modification"
// OperationTypeNew 表示新建
OperationTypeNew = "new"
// OperationTypeAccess 表示访问
OperationTypeAccess = "access"
)
// CheckLogType 检查日志类型是否合法
func CheckLogType(logType string) bool {
switch logType {
case LogTypeOperation, LogTypeAccess:
return true
}
return false
}
// CheckResourceType 检查资源类型是否合法
func CheckResourceType(resourceType string) bool {
switch resourceType {
case ResourceTypeAuditLog,
ResourceTypeAccountCenter,
ResourceTypeClusterManagement,
ResourceTypeDevTool,
ResourceTypeDataMigration:
return true
}
return false
}
// CheckOperationType 检查操作类型是否合法
func CheckOperationType(operationType string) bool {
switch operationType {
case OperationTypeLogin, OperationTypeDeletion,
OperationTypeModification, OperationTypeNew, OperationTypeAccess:
return true
}
return false
}
// AuditLog 表示审计日志
type AuditLog struct {
// 日志 ID插入后返回
ID string `json:"id,omitempty" elastic_meta:"_id" elastic_mapping:"id:{type:keyword}"`
// 日志条目的创建时间
Timestamp time.Time `json:"timestamp" elastic_mapping:"timestamp:{type:date}"`
// 日志元数据
Metadata AuditLogMetadata `json:"metadata" elastic_mapping:"metadata:{type:object}"`
}
// AuditLogMetadata 表示审计日志元数据
type AuditLogMetadata struct {
// 操作者。比如 Zora
Operator string `json:"operator"`
// 日志类型。比如操作日志
LogType string `json:"log_type" elastic_mapping:"log_type:{type:keyword}"`
// 资源类型。比如审计日志
ResourceType string `json:"resource_type" elastic_mapping:"resource_type:{type:keyword}"`
// 事件定义。包括:
// - event_name事件名称。比如删除 ES 集群实例
// - event_source_ip事件源 IP。比如 175.0.24.182(中国 湖南省 长沙市 中国电信)
// - team团队。比如运维团队
// - resource_name资源名称。比如 es-7104
// - operation操作。比如删除
// - event_record事件记录。比如操作语句和查询参数
Labels util.MapStr `json:"labels" elastic_mapping:"labels:{type:object}"`
}
// Reset 将一些字段重置。比如在插入前清空 ID 等
func (log *AuditLog) Reset() *AuditLog {
log.ID = ""
if log.Timestamp.IsZero() {
log.Timestamp = time.Now()
}
return log
}
// Validate 检查字段是否合法
func (log *AuditLog) Validate() error {
if log.Timestamp.IsZero() {
return ErrCreatedTimeNotProvided
}
// 操作者不能为空
if log.Metadata.Operator == "" {
return ErrOperatorNotProvided
}
if !CheckLogType(log.Metadata.LogType) {
return ErrLogTypeNotProvided
}
if !CheckResourceType(log.Metadata.ResourceType) {
return ErrResourceTypeNotProvided
}
if log.Metadata.Labels == nil {
return ErrEventNotProvided
}
// 事件名称可以为空
// 检查事件源 IP 是否合法
eventSourceIPAny, found := log.Metadata.Labels["event_source_ip"]
if !found {
return ErrEventSourceIPNotProvided
}
eventSourceIP, ok := eventSourceIPAny.(string)
if !ok {
return ErrEventSourceIPNotProvided
}
leftParenthesisIndex := strings.Index(eventSourceIP, "(")
if leftParenthesisIndex < 0 {
// 如果没有括号,那么将字段视为 IP 地址
if net.ParseIP(eventSourceIP) == nil {
return ErrEventSourceIPNotProvided
}
} else {
ipPart := eventSourceIP[:leftParenthesisIndex]
if net.ParseIP(ipPart) == nil {
return ErrEventSourceIPNotProvided
}
ipPlacePart := eventSourceIP[leftParenthesisIndex:]
if len(ipPlacePart) < 2 {
// 允许 IP 归属地为空
return ErrEventSourceIPNotProvided
}
}
// 团队可以为空
// 资源名称可以为空
operationAny, found := log.Metadata.Labels["operation"]
if !found {
return ErrOperationNotProvided
}
operation, ok := operationAny.(string)
if !ok || !CheckOperationType(operation) {
return ErrOperationNotProvided
}
// 事件记录可以为空
// ID 可以为空
return nil
}
// AuditLogBuilder 用于构造 AuditLog 实例
type AuditLogBuilder struct {
id string
timestamp time.Time
metaData AuditLogMetadata
}
// NewAuditLogBuilder 构造空的 AuditLogBuilder 实例
func NewAuditLogBuilder() *AuditLogBuilder {
builder := new(AuditLogBuilder)
builder.metaData.Labels = make(util.MapStr)
return builder
}
// NewAuditLogBuilderWithDefault 构造带默认值的 Builder
func NewAuditLogBuilderWithDefault() *AuditLogBuilder {
return NewAuditLogBuilder().
WithOperator("").
WithLogTypeAccess().
WithResourceTypeAuditLog().
WithEventName("").
WithTimestampNow().
WithEventSourceIP(net.IPv4zero.String()).
WithTeam("").
WithResourceName("").
WithOperationTypeAccess().
WithEventRecord("")
}
// WithID 设置 ID
func (builder *AuditLogBuilder) WithID(id string) *AuditLogBuilder {
builder.id = id
return builder
}
// WithOperator 设置操作者
func (builder *AuditLogBuilder) WithOperator(operator string) *AuditLogBuilder {
if operator == "" {
operator = "unknown"
}
builder.metaData.Operator = operator
return builder
}
// WithLogTypeOperation 将日志类型日志设置为操作日志
func (builder *AuditLogBuilder) WithLogTypeOperation() *AuditLogBuilder {
builder.metaData.LogType = LogTypeOperation
return builder
}
// WithLogTypeAccess 将日志类型设置为访问日志
func (builder *AuditLogBuilder) WithLogTypeAccess() *AuditLogBuilder {
builder.metaData.LogType = LogTypeAccess
return builder
}
// WithResourceTypeAuditLog 将资源类型设置为审计日志
func (builder *AuditLogBuilder) WithResourceTypeAuditLog() *AuditLogBuilder {
builder.metaData.ResourceType = ResourceTypeAuditLog
return builder
}
// WithResourceTypeAccountCenter 将资源类型设置为账户中心
func (builder *AuditLogBuilder) WithResourceTypeAccountCenter() *AuditLogBuilder {
builder.metaData.ResourceType = ResourceTypeAccountCenter
return builder
}
// WithResourceTypeClusterManagement 将资源类型设置为集群管理
func (builder *AuditLogBuilder) WithResourceTypeClusterManagement() *AuditLogBuilder {
builder.metaData.ResourceType = ResourceTypeClusterManagement
return builder
}
// WithResourceTypeDevTool 将资源类型设置为开发工具
func (builder *AuditLogBuilder) WithResourceTypeDevTool() *AuditLogBuilder {
builder.metaData.ResourceType = ResourceTypeDevTool
return builder
}
// WithResourceTypeDataMigration 将资源类型设置为数据迁移
func (builder *AuditLogBuilder) WithResourceTypeDataMigration() *AuditLogBuilder {
builder.metaData.ResourceType = ResourceTypeDataMigration
return builder
}
// WithEventName 设置事件名称
func (builder *AuditLogBuilder) WithEventName(eventName string) *AuditLogBuilder {
builder.metaData.Labels["event_name"] = eventName
return builder
}
// WithTimestamp 设置时间戳
func (builder *AuditLogBuilder) WithTimestamp(eventTime time.Time) *AuditLogBuilder {
builder.timestamp = eventTime
return builder
}
// WithTimestampNow 将时间戳设置为当前时间
func (builder *AuditLogBuilder) WithTimestampNow() *AuditLogBuilder {
return builder.WithTimestamp(time.Now())
}
// WithEventSourceIPAndPlace 设置事件源 IP 和 IP 归属地
func (builder *AuditLogBuilder) WithEventSourceIPAndPlace(eventSourceIP, ipPlace string) *AuditLogBuilder {
if net.ParseIP(eventSourceIP) == nil {
// 对于无效 IP保存为 0.0.0.0
eventSourceIP = net.IPv4zero.String()
}
// 如果 IP 归属地为空,那么省略括号中的部分
s := eventSourceIP
if ipPlace != "" {
s = fmt.Sprintf("%s(%s)", eventSourceIP, ipPlace)
}
builder.metaData.Labels["event_source_ip"] = s
return builder
}
// WithEventSourceIP 设置事件源 IP将根据 IP 地址获取其归属地
func (builder *AuditLogBuilder) WithEventSourceIP(eventSourceIP string) *AuditLogBuilder {
ipPlace := ""
// TODO根据 IP 地址获取归属地
return builder.WithEventSourceIPAndPlace(eventSourceIP, ipPlace)
}
// WithTeam 设置团队
func (builder *AuditLogBuilder) WithTeam(team string) *AuditLogBuilder {
builder.metaData.Labels["team"] = team
return builder
}
// WithResourceName 设置资源名称
func (builder *AuditLogBuilder) WithResourceName(resourceName string) *AuditLogBuilder {
builder.metaData.Labels["resource_name"] = resourceName
return builder
}
// WithOperationTypeLogin 将操作类型设置为登录
func (builder *AuditLogBuilder) WithOperationTypeLogin() *AuditLogBuilder {
builder.metaData.Labels["operation"] = OperationTypeLogin
return builder
}
// WithOperationTypeDeletion 将操作类型设置为删除
func (builder *AuditLogBuilder) WithOperationTypeDeletion() *AuditLogBuilder {
builder.metaData.Labels["operation"] = OperationTypeDeletion
return builder
}
// WithOperationTypeModification 将操作类型设置为修改
func (builder *AuditLogBuilder) WithOperationTypeModification() *AuditLogBuilder {
builder.metaData.Labels["operation"] = OperationTypeModification
return builder
}
// WithOperationTypeNew 将操作类型设置为新建
func (builder *AuditLogBuilder) WithOperationTypeNew() *AuditLogBuilder {
builder.metaData.Labels["operation"] = OperationTypeNew
return builder
}
// WithOperationTypeAccess 将操作类型设置为访问
func (builder *AuditLogBuilder) WithOperationTypeAccess() *AuditLogBuilder {
builder.metaData.Labels["operation"] = OperationTypeAccess
return builder
}
// WithEventRecord 设置事件记录,比如操作语句或查询参数
func (builder *AuditLogBuilder) WithEventRecord(eventRecord string) *AuditLogBuilder {
builder.metaData.Labels["event_record"] = eventRecord
return builder
}
func (builder *AuditLogBuilder) Build() (a *AuditLog, err error) {
// 构造对象
a = new(AuditLog)
a.ID = builder.id
a.Timestamp = builder.timestamp
a.Metadata = builder.metaData
// 校验构造的对象
err = a.Validate()
return
}

View File

@ -3,6 +3,10 @@ package index_management
import (
"fmt"
log "github.com/cihub/seelog"
"infini.sh/console/common"
"infini.sh/console/model"
"infini.sh/console/service"
"infini.sh/framework/core/api/rbac"
httprouter "infini.sh/framework/core/api/router"
"infini.sh/framework/core/elastic"
"infini.sh/framework/core/event"
@ -17,9 +21,9 @@ import (
func (handler APIHandler) ElasticsearchOverviewAction(w http.ResponseWriter, req *http.Request, ps httprouter.Params) {
var (
totalNode int
totalNode int
totalStoreSize int
clusterIDs []interface{}
clusterIDs []interface{}
)
//elastic.WalkConfigs(func(key, value interface{})bool{
// if handler.Config.Elasticsearch == key {
@ -33,13 +37,13 @@ func (handler APIHandler) ElasticsearchOverviewAction(w http.ResponseWriter, req
"size": 100,
}
clusterFilter, hasAllPrivilege := handler.GetClusterFilter(req, "_id")
if !hasAllPrivilege && clusterFilter == nil{
if !hasAllPrivilege && clusterFilter == nil {
handler.WriteJSON(w, util.MapStr{
"nodes_count": 0,
"clusters_count":0,
"nodes_count": 0,
"clusters_count": 0,
"total_used_store_in_bytes": 0,
"hosts_count": 0,
"indices_count": 0,
"hosts_count": 0,
"indices_count": 0,
}, http.StatusOK)
return
}
@ -47,6 +51,16 @@ func (handler APIHandler) ElasticsearchOverviewAction(w http.ResponseWriter, req
queryDsl["query"] = clusterFilter
}
user, auditLogErr := rbac.FromUserContext(req.Context())
if auditLogErr == nil && handler.GetHeader(req, "Referer", "") != "" {
auditLog, _ := model.NewAuditLogBuilderWithDefault().WithOperator(user.Username).
WithLogTypeAccess().WithResourceTypeClusterManagement().
WithEventName("get elasticsearch overview").WithEventSourceIP(common.GetClientIP(req)).
WithResourceName("elasticsearch").WithOperationTypeAccess().
WithEventRecord(util.MustToJSON(queryDsl)).Build()
_ = service.LogAuditLog(auditLog)
}
searchRes, err := esClient.SearchWithRawQueryDSL(orm.GetIndexName(elastic.ElasticsearchConfig{}), util.MustToJSONBytes(queryDsl))
if err != nil {
log.Error(err)
@ -86,39 +100,39 @@ func (handler APIHandler) ElasticsearchOverviewAction(w http.ResponseWriter, req
}
hostCount, err := handler.getMetricCount(orm.GetIndexName(host.HostInfo{}), "ip", nil)
if err != nil{
if err != nil {
log.Error(err)
}
if v, ok := hostCount.(float64); (ok && v == 0) || hostCount == nil {
hostCount, err = handler.getMetricCount(orm.GetIndexName(elastic.NodeConfig{}), "metadata.host", clusterIDs)
if err != nil{
if err != nil {
log.Error(err)
}
}
nodeCount, err := handler.getMetricCount(orm.GetIndexName(elastic.NodeConfig{}), "id", clusterIDs)
if err != nil{
if err != nil {
log.Error(err)
}
if v, ok := nodeCount.(float64); ok {
totalNode = int(v)
}
indicesCount, err := handler.getIndexCount(req)
if err != nil{
if err != nil {
log.Error(err)
}
resBody := util.MapStr{
"nodes_count": totalNode,
"clusters_count": len(clusterIDs),
"nodes_count": totalNode,
"clusters_count": len(clusterIDs),
"total_used_store_in_bytes": totalStoreSize,
"hosts_count": hostCount,
"indices_count": indicesCount,
"hosts_count": hostCount,
"indices_count": indicesCount,
}
handler.WriteJSON(w, resBody, http.StatusOK)
}
func (handler APIHandler) getLatestClusterMonitorData(clusterIDs []interface{}) (*elastic.SearchResponse, error){
func (handler APIHandler) getLatestClusterMonitorData(clusterIDs []interface{}) (*elastic.SearchResponse, error) {
client := elastic.GetClient(global.MustLookupString(elastic.GlobalSystemElasticsearchID))
queryDSLTpl := `{
"size": %d,
@ -243,18 +257,18 @@ func (handler APIHandler) getIndexCount(req *http.Request) (int64, error) {
return orm.Count(elastic.IndexConfig{}, body)
}
func (handler APIHandler) getMetricCount(indexName, field string, clusterIDs []interface{}) (interface{}, error){
func (handler APIHandler) getMetricCount(indexName, field string, clusterIDs []interface{}) (interface{}, error) {
client := elastic.GetClient(global.MustLookupString(elastic.GlobalSystemElasticsearchID))
queryDSL := util.MapStr{
"size": 0,
"aggs": util.MapStr{
"field_count": util.MapStr{
"cardinality": util.MapStr{
"field": field,
},
},
},
}
"size": 0,
"aggs": util.MapStr{
"field_count": util.MapStr{
"cardinality": util.MapStr{
"field": field,
},
},
},
}
if len(clusterIDs) > 0 {
queryDSL["query"] = util.MapStr{
"terms": util.MapStr{
@ -270,7 +284,7 @@ func (handler APIHandler) getMetricCount(indexName, field string, clusterIDs []i
return searchRes.Aggregations["field_count"].Value, nil
}
func (handler APIHandler) getLastActiveHostCount() (int, error){
func (handler APIHandler) getLastActiveHostCount() (int, error) {
client := elastic.GetClient(global.MustLookupString(elastic.GlobalSystemElasticsearchID))
queryDSL := `{
"size": 0,
@ -321,12 +335,12 @@ func (handler APIHandler) getLastActiveHostCount() (int, error){
return len(searchRes.Aggregations["week_active_host"].Buckets), nil
}
func (handler APIHandler) ElasticsearchStatusSummaryAction(w http.ResponseWriter, req *http.Request, ps httprouter.Params){
func (handler APIHandler) ElasticsearchStatusSummaryAction(w http.ResponseWriter, req *http.Request, ps httprouter.Params) {
clusterIDs, hasAllPrivilege := handler.GetAllowedClusters(req)
if !hasAllPrivilege && len(clusterIDs) == 0{
if !hasAllPrivilege && len(clusterIDs) == 0 {
handler.WriteJSON(w, util.MapStr{
"cluster": util.MapStr{},
"node": util.MapStr{},
"node": util.MapStr{},
"host": util.MapStr{
"online": 0,
},
@ -375,14 +389,14 @@ func (handler APIHandler) ElasticsearchStatusSummaryAction(w http.ResponseWriter
}
handler.WriteJSON(w, util.MapStr{
"cluster": clusterGrp,
"node": nodeGrp,
"node": nodeGrp,
"host": util.MapStr{
"online": hostCount,
},
}, http.StatusOK)
}
func (handler APIHandler) getGroupMetric(indexName, field string, filter interface{}) (interface{}, error){
func (handler APIHandler) getGroupMetric(indexName, field string, filter interface{}) (interface{}, error) {
client := elastic.GetClient(global.MustLookupString(elastic.GlobalSystemElasticsearchID))
queryDSL := util.MapStr{
"size": 0,
@ -561,4 +575,4 @@ func (h *APIHandler) ClusterOverTreeMap(w http.ResponseWriter, req *http.Request
}
h.Write(w, util.MustToJSONBytes(result))
}
}

View File

@ -2,6 +2,10 @@ package index_management
import (
log "github.com/cihub/seelog"
"infini.sh/console/common"
"infini.sh/console/model"
"infini.sh/console/service"
"infini.sh/framework/core/api/rbac"
httprouter "infini.sh/framework/core/api/router"
"infini.sh/framework/core/elastic"
"infini.sh/framework/core/radix"
@ -39,11 +43,19 @@ func (handler APIHandler) HandleGetMappingsAction(w http.ResponseWriter, req *ht
func (handler APIHandler) HandleCatIndicesAction(w http.ResponseWriter, req *http.Request, ps httprouter.Params) {
targetClusterID := ps.ByName("id")
user, auditLogErr := rbac.FromUserContext(req.Context())
if auditLogErr == nil && handler.GetHeader(req, "Referer", "") != "" {
auditLog, _ := model.NewAuditLogBuilderWithDefault().WithOperator(user.Username).
WithLogTypeAccess().WithResourceTypeClusterManagement().
WithEventName("get indices").WithEventSourceIP(common.GetClientIP(req)).
WithResourceName(targetClusterID).WithOperationTypeAccess().WithEventRecord("").Build()
_ = service.LogAuditLog(auditLog)
}
client := elastic.GetClient(targetClusterID)
//filter indices
allowedIndices, hasAllPrivilege := handler.GetAllowedIndices(req, targetClusterID)
if !hasAllPrivilege && len(allowedIndices) == 0 {
handler.WriteJSON(w, []interface{}{} , http.StatusOK)
handler.WriteJSON(w, []interface{}{}, http.StatusOK)
return
}
catIndices, err := client.GetIndices("")
@ -58,7 +70,7 @@ func (handler APIHandler) HandleCatIndicesAction(w http.ResponseWriter, req *htt
filterIndices := map[string]elastic.IndexInfo{}
pattern := radix.Compile(allowedIndices...)
for indexName, indexInfo := range *catIndices {
if pattern.Match(indexName){
if pattern.Match(indexName) {
filterIndices[indexName] = indexInfo
}
}
@ -123,10 +135,18 @@ func (handler APIHandler) HandleDeleteIndexAction(w http.ResponseWriter, req *ht
}
func (handler APIHandler) HandleCreateIndexAction(w http.ResponseWriter, req *http.Request, ps httprouter.Params) {
targetClusterID := ps.ByName("id")
client := elastic.GetClient(targetClusterID)
indexName := ps.ByName("index")
claims, auditLogErr := rbac.ValidateLogin(req.Header.Get("Authorization"))
if auditLogErr == nil && handler.GetHeader(req, "Referer", "") != "" {
auditLog, _ := model.NewAuditLogBuilderWithDefault().WithOperator(claims.Username).
WithLogTypeOperation().WithResourceTypeClusterManagement().
WithEventName("create index").WithEventSourceIP(common.GetClientIP(req)).
WithResourceName(targetClusterID).WithOperationTypeNew().WithEventRecord(indexName).Build()
auditLogErr = service.LogAuditLog(auditLog)
}
resBody := newResponseBody()
config := map[string]interface{}{}
err := handler.DecodeJSON(req, &config)
@ -159,4 +179,4 @@ func (handler APIHandler) HandleGetIndexAction(w http.ResponseWriter, req *http.
}
handler.WriteJSON(w, indexRes, http.StatusOK)
}
}

View File

@ -1,20 +1,19 @@
package api
import (
"infini.sh/console/plugin/api/data"
"infini.sh/console/plugin/api/email"
"infini.sh/console/plugin/api/license"
"infini.sh/console/plugin/api/platform"
"path"
"infini.sh/console/config"
"infini.sh/console/plugin/api/alerting"
"infini.sh/console/plugin/api/data"
"infini.sh/console/plugin/api/email"
"infini.sh/console/plugin/api/index_management"
"infini.sh/console/plugin/api/insight"
"infini.sh/console/plugin/api/layout"
"infini.sh/console/plugin/api/license"
"infini.sh/console/plugin/api/notification"
"infini.sh/console/plugin/api/platform"
"infini.sh/framework/core/api"
"infini.sh/framework/core/api/rbac/enum"
"path"
)
func Init(cfg *config.AppConfig) {

View File

@ -7,6 +7,9 @@ package platform
import (
"fmt"
log "github.com/cihub/seelog"
"infini.sh/console/common"
"infini.sh/console/model"
"infini.sh/console/service"
"infini.sh/framework/core/api"
"infini.sh/framework/core/api/rbac"
httprouter "infini.sh/framework/core/api/router"
@ -33,13 +36,13 @@ func (h *PlatformAPI) searchCollection(w http.ResponseWriter, req *http.Request,
collMetas := GetCollectionMetas()
var (
meta CollectionMeta
ok bool
ok bool
)
if meta, ok = collMetas[collName]; !ok {
h.WriteError(w, fmt.Sprintf("metadata of collection [%s] not found", collName), http.StatusInternalServerError)
return
}
if api.IsAuthEnable(){
if api.IsAuthEnable() {
claims, err := rbac.ValidateLogin(req.Header.Get("Authorization"))
if err != nil {
h.WriteError(w, err.Error(), http.StatusUnauthorized)
@ -61,8 +64,7 @@ func (h *PlatformAPI) searchCollection(w http.ResponseWriter, req *http.Request,
if meta.GetSearchRequestBodyFilter != nil {
filter, hasAllPrivilege := meta.GetSearchRequestBodyFilter(h, req)
if !hasAllPrivilege && filter == nil {
h.WriteJSON(w, elastic.SearchResponse{
}, http.StatusOK)
h.WriteJSON(w, elastic.SearchResponse{}, http.StatusOK)
return
}
if !hasAllPrivilege {
@ -84,10 +86,10 @@ func (h *PlatformAPI) searchCollection(w http.ResponseWriter, req *http.Request,
h.WriteError(w, string(searchRes.RawResult.Body), http.StatusInternalServerError)
return
}
h.WriteJSON(w, searchRes,http.StatusOK)
h.WriteJSON(w, searchRes, http.StatusOK)
}
func (h *PlatformAPI) rewriteQueryWithFilter(queryDsl []byte, filter util.MapStr) ([]byte, error){
func (h *PlatformAPI) rewriteQueryWithFilter(queryDsl []byte, filter util.MapStr) ([]byte, error) {
mapObj := util.MapStr{}
err := util.FromJSONBytes(queryDsl, &mapObj)
@ -122,13 +124,23 @@ func (h *PlatformAPI) rewriteQueryWithFilter(queryDsl []byte, filter util.MapStr
return queryDsl, nil
}
//getCollectionMeta returns metadata of target collection, includes backend index name
// getCollectionMeta returns metadata of target collection, includes backend index name
func (h *PlatformAPI) getCollectionMeta(w http.ResponseWriter, req *http.Request, ps httprouter.Params) {
collName := ps.MustGetParameter("collection_name")
if collName == "activity" {
user, auditLogErr := rbac.FromUserContext(req.Context())
if auditLogErr == nil && h.GetHeader(req, "Referer", "") != "" {
auditLog, _ := model.NewAuditLogBuilderWithDefault().WithOperator(user.Username).
WithLogTypeAccess().WithResourceTypeAccountCenter().
WithEventName("get activity meta").WithEventSourceIP(common.GetClientIP(req)).
WithResourceName("activity").WithOperationTypeAccess().WithEventRecord("").Build()
_ = service.LogAuditLog(auditLog)
}
}
collMetas := GetCollectionMetas()
var (
meta CollectionMeta
ok bool
ok bool
)
if meta, ok = collMetas[collName]; !ok {
h.WriteError(w, fmt.Sprintf("metadata of collection [%s] not found", collName), http.StatusInternalServerError)
@ -141,4 +153,4 @@ func (h *PlatformAPI) getCollectionMeta(w http.ResponseWriter, req *http.Request
"index_name": indexName,
},
}, http.StatusOK)
}
}

View File

@ -5,6 +5,7 @@
package platform
import (
consoleModel "infini.sh/console/model"
"infini.sh/console/model/alerting"
"infini.sh/framework/core/api/rbac/enum"
"infini.sh/framework/core/elastic"
@ -18,10 +19,10 @@ import (
var (
collectionMetas map[string]CollectionMeta
metasInitOnce sync.Once
metasInitOnce sync.Once
)
func GetCollectionMetas() map[string]CollectionMeta{
func GetCollectionMetas() map[string]CollectionMeta {
metasInitOnce.Do(func() {
collectionMetas = map[string]CollectionMeta{
"gateway": {
@ -108,10 +109,10 @@ func GetCollectionMetas() map[string]CollectionMeta{
for clusterID, indices := range indexPrivilege {
var (
wildcardIndices []string
normalIndices []string
normalIndices []string
)
for _, index := range indices {
if strings.Contains(index,"*") {
if strings.Contains(index, "*") {
wildcardIndices = append(wildcardIndices, index)
continue
}
@ -121,8 +122,8 @@ func GetCollectionMetas() map[string]CollectionMeta{
if len(wildcardIndices) > 0 {
subShould = append(subShould, util.MapStr{
"query_string": util.MapStr{
"query": strings.Join(wildcardIndices, " "),
"fields": []string{"metadata.labels.index_name"},
"query": strings.Join(wildcardIndices, " "),
"fields": []string{"metadata.labels.index_name"},
"default_operator": "OR",
},
})
@ -147,7 +148,7 @@ func GetCollectionMetas() map[string]CollectionMeta{
{
"bool": util.MapStr{
"minimum_should_match": 1,
"should": subShould,
"should": subShould,
},
},
},
@ -157,7 +158,7 @@ func GetCollectionMetas() map[string]CollectionMeta{
indexFilter := util.MapStr{
"bool": util.MapStr{
"minimum_should_match": 1,
"should": indexShould,
"should": indexShould,
},
}
filter = append(filter, indexFilter)
@ -169,6 +170,15 @@ func GetCollectionMetas() map[string]CollectionMeta{
}, hasAllPrivilege
},
},
"audit_log": {
Name: "audit_log",
RequirePermission: map[string][]string{
"read": {
enum.PermissionAuditLogRead,
},
},
MatchObject: &consoleModel.AuditLog{},
},
"alerting_rule": {
Name: "alerting_rule",
RequirePermission: map[string][]string{
@ -182,7 +192,8 @@ func GetCollectionMetas() map[string]CollectionMeta{
})
return collectionMetas
}
//CollectionMeta includes information about how to visit backend index
// CollectionMeta includes information about how to visit backend index
type CollectionMeta struct {
//collection name
Name string `json:"name"`
@ -194,4 +205,4 @@ type CollectionMeta struct {
GetSearchRequestBodyFilter SearchRequestBodyFilter
}
type SearchRequestBodyFilter func(api *PlatformAPI, req *http.Request) (util.MapStr, bool)
type SearchRequestBodyFilter func(api *PlatformAPI, req *http.Request) (util.MapStr, bool)

View File

@ -0,0 +1,55 @@
/* Copyright © INFINI Ltd. All rights reserved.
* Web: https://infinilabs.com
* Email: hello#infini.ltd */
package audit_log
import (
"context"
"infini.sh/console/common"
"infini.sh/console/model"
"infini.sh/console/service"
"infini.sh/framework/core/api"
"infini.sh/framework/core/api/rbac"
"net/http"
"regexp"
"strings"
)
var overviewRegexp = regexp.MustCompile(`/elasticsearch/([^/]+)/(cluster_metrics|metrics|nodes/realtime|indices/realtime)`)
var _ api.Interceptor = (*MonitoringInterceptor)(nil)
type MonitoringInterceptor struct{}
func (m *MonitoringInterceptor) Match(request *http.Request) bool {
return overviewRegexp.MatchString(request.URL.Path)
}
func (m *MonitoringInterceptor) PreHandle(c context.Context, _ http.ResponseWriter,
request *http.Request) (context.Context, error) {
handler := &api.Handler{}
targetClusterID := ""
eventName := ""
matches := overviewRegexp.FindStringSubmatch(request.URL.Path)
if len(matches) > 1 {
targetClusterID = matches[1]
eventName = strings.Replace(matches[2], "/", " ", -1)
}
claims, auditLogErr := rbac.ValidateLogin(request.Header.Get("Authorization"))
if auditLogErr == nil && handler.GetHeader(request, "Referer", "") != "" {
auditLog, _ := model.NewAuditLogBuilderWithDefault().WithOperator(claims.Username).
WithLogTypeAccess().WithResourceTypeClusterManagement().
WithEventName("monitoring " + eventName).WithEventSourceIP(common.GetClientIP(request)).
WithResourceName(targetClusterID).WithOperationTypeAccess().WithEventRecord(request.URL.RawQuery).Build()
_ = service.LogAuditLog(auditLog)
}
return c, nil
}
func (m *MonitoringInterceptor) PostHandle(_ context.Context, _ http.ResponseWriter, _ *http.Request) {
}
func (m *MonitoringInterceptor) Name() string {
return "monitoring_interceptor"
}

39
service/audit_log.go Normal file
View File

@ -0,0 +1,39 @@
/* Copyright © INFINI Ltd. All rights reserved.
* Web: https://infinilabs.com
* Email: hello#infini.ltd */
package service
import (
"infini.sh/console/model"
"infini.sh/framework/core/queue"
"infini.sh/framework/core/util"
)
const AuditLogQueueName = "logging-audit-log-queue"
type AuditLogAction struct {
log *model.AuditLog
}
func NewAuditLogAction(log *model.AuditLog) *AuditLogAction {
return &AuditLogAction{
log: log,
}
}
func (action AuditLogAction) Execute() ([]byte, error) {
queueCfg := queue.GetOrInitConfig(AuditLogQueueName)
msg := util.MustToJSONBytes(action.log)
err := queue.Push(queueCfg, msg)
return nil, err
}
// LogAuditLog 记录审计日志
func LogAuditLog(log *model.AuditLog) (err error) {
if err = log.Validate(); err != nil {
return err
}
_, err = NewAuditLogAction(log).Execute()
return
}