From 10c317f07c2ed69e9f049a5b63a85125a295d1f8 Mon Sep 17 00:00:00 2001 From: root Date: Fri, 12 Apr 2024 06:31:13 +0000 Subject: [PATCH] feat: add audit logs --- common/audit_log.go | 29 ++ config/setup/easysearch/template_ilm.tpl | 105 +++++ config/setup/elasticsearch/template_ilm.tpl | 105 +++++ .../setup/elasticsearch/v5/template_ilm.tpl | 102 +++++ .../setup/elasticsearch/v6/template_ilm.tpl | 109 ++++++ config/setup/opensearch/template_ilm.tpl | 105 +++++ config/system_config.tpl | 16 + main.go | 8 +- model/audit_log.go | 369 ++++++++++++++++++ plugin/api/index_management/elasticsearch.go | 80 ++-- plugin/api/index_management/indices.go | 28 +- plugin/api/init.go | 11 +- plugin/api/platform/api.go | 30 +- plugin/api/platform/domain.go | 31 +- plugin/audit_log/monitoring_interceptor.go | 55 +++ service/audit_log.go | 39 ++ 16 files changed, 1157 insertions(+), 65 deletions(-) create mode 100644 common/audit_log.go create mode 100644 model/audit_log.go create mode 100644 plugin/audit_log/monitoring_interceptor.go create mode 100644 service/audit_log.go diff --git a/common/audit_log.go b/common/audit_log.go new file mode 100644 index 00000000..6423849d --- /dev/null +++ b/common/audit_log.go @@ -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] +} diff --git a/config/setup/easysearch/template_ilm.tpl b/config/setup/easysearch/template_ilm.tpl index 683e3e8e..5b69a170 100644 --- a/config/setup/easysearch/template_ilm.tpl +++ b/config/setup/easysearch/template_ilm.tpl @@ -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 + } + } } \ No newline at end of file diff --git a/config/setup/elasticsearch/template_ilm.tpl b/config/setup/elasticsearch/template_ilm.tpl index 8fa71f7d..769f3b85 100644 --- a/config/setup/elasticsearch/template_ilm.tpl +++ b/config/setup/elasticsearch/template_ilm.tpl @@ -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 + } + } } \ No newline at end of file diff --git a/config/setup/elasticsearch/v5/template_ilm.tpl b/config/setup/elasticsearch/v5/template_ilm.tpl index 0a0cf52f..f73170a3 100644 --- a/config/setup/elasticsearch/v5/template_ilm.tpl +++ b/config/setup/elasticsearch/v5/template_ilm.tpl @@ -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 + } + } } \ No newline at end of file diff --git a/config/setup/elasticsearch/v6/template_ilm.tpl b/config/setup/elasticsearch/v6/template_ilm.tpl index 9f2c1b6c..fbf1d950 100644 --- a/config/setup/elasticsearch/v6/template_ilm.tpl +++ b/config/setup/elasticsearch/v6/template_ilm.tpl @@ -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 + } + } } \ No newline at end of file diff --git a/config/setup/opensearch/template_ilm.tpl b/config/setup/opensearch/template_ilm.tpl index e2011552..13b92b7c 100644 --- a/config/setup/opensearch/template_ilm.tpl +++ b/config/setup/opensearch/template_ilm.tpl @@ -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" } \ No newline at end of file diff --git a/config/system_config.tpl b/config/system_config.tpl index d78fdde6..8d625826 100644 --- a/config/system_config.tpl +++ b/config/system_config.tpl @@ -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 diff --git a/main.go b/main.go index 9a310c13..4b299bce 100644 --- a/main.go +++ b/main.go @@ -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 { diff --git a/model/audit_log.go b/model/audit_log.go new file mode 100644 index 00000000..ca7d8ac7 --- /dev/null +++ b/model/audit_log.go @@ -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 +} diff --git a/plugin/api/index_management/elasticsearch.go b/plugin/api/index_management/elasticsearch.go index eb75eccf..ab792a9d 100644 --- a/plugin/api/index_management/elasticsearch.go +++ b/plugin/api/index_management/elasticsearch.go @@ -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)) -} \ No newline at end of file +} diff --git a/plugin/api/index_management/indices.go b/plugin/api/index_management/indices.go index 51aec6d9..54d923eb 100644 --- a/plugin/api/index_management/indices.go +++ b/plugin/api/index_management/indices.go @@ -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) -} \ No newline at end of file +} diff --git a/plugin/api/init.go b/plugin/api/init.go index dc3cb319..09fec3e3 100644 --- a/plugin/api/init.go +++ b/plugin/api/init.go @@ -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) { diff --git a/plugin/api/platform/api.go b/plugin/api/platform/api.go index cab75663..c91b5187 100644 --- a/plugin/api/platform/api.go +++ b/plugin/api/platform/api.go @@ -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) -} \ No newline at end of file +} diff --git a/plugin/api/platform/domain.go b/plugin/api/platform/domain.go index f12ceefa..240d0d81 100644 --- a/plugin/api/platform/domain.go +++ b/plugin/api/platform/domain.go @@ -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) \ No newline at end of file +type SearchRequestBodyFilter func(api *PlatformAPI, req *http.Request) (util.MapStr, bool) diff --git a/plugin/audit_log/monitoring_interceptor.go b/plugin/audit_log/monitoring_interceptor.go new file mode 100644 index 00000000..566893c8 --- /dev/null +++ b/plugin/audit_log/monitoring_interceptor.go @@ -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" +} diff --git a/service/audit_log.go b/service/audit_log.go new file mode 100644 index 00000000..75389193 --- /dev/null +++ b/service/audit_log.go @@ -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 +}