Hibernate 的 Envers 相当于 golang (GORM),用于审计

问题描述 投票:0回答:1

我们想要跟踪数据库(PostgreSQL)中各个表的所有条目的操作(审核)历史记录。编程语言是 golang,GORM ( https://gorm.io/ ) 作为 ORM。

基本上在单独的(例如audit_logs)表中捕获表上的CREATE、UPDATE 和DELETE 操作。

audit_logs 表所需的架构是:

  • entity_class // 发生操作的表
  • entity_id // 发生操作的主键值,基本上是行的引用
  • action // 操作,例如创建、更新或删除
  • action_taker // 完成该操作的 user_id(可以从请求的上下文中检索)
  • prev_data // 操作前数据的 json 二进制文件,(CREATE 时为 NULL)
  • cur_data // 运算后的json二进制数据
  • at // 操作发生的时间

Hibernate 的 enver 中提供了此类功能 ( https://www.baeldung.com/database-auditing-jpa#hibernate )。

但是,对于golang的GORM来说,没有任何这样的功能。

有人在 golang 中遇到过这样的要求吗?如果是,找到/构建的方法或解决方案是什么?任何线索都会有帮助。

我们尝试在 gorm.DB 对象上注册钩子。

说audit_log的对象是

import (
    "gorm.io/gorm"
)

type AuditLog struct {
    EntityClass  string `gorm:"column:entity_class"`
    EntityID     string `gorm:"column:entity_id"`
    Action       string `gorm:"column:action"`
    ActionTaker  string `gorm:"column:action_taker"`
    PrevData     interface{} `gorm:"column:prev_data"`
    CurData      interface{} `gorm:"column:cur_data"`
    At           time.Time `gorm:"column:at"`
}

现在,在数据库的初始化函数中,注册钩子

    _ = db.AutoMigrate(&AuditLog{})  // creating the table if not exists

    _ = db.Callback().Create().After("gorm:create").Register("audit_log:create", auditLogCreateCallback)
    _ = db.Callback().Update().After("gorm:update").Register("audit_log:update", auditLogUpdateCallback)
    _ = db.Callback().Delete().After("gorm:delete").Register("audit_log:delete", auditLogDeleteCallback)

并定义了各自的功能,例如,

func auditLogCreateCallback(db *gorm.DB) {
    if db.Error != nil {
        return
    }

    entityName := db.Statement.Table

    // here we need to extract the information from db.Statement object (OR other location if required)
    // basically we need EntityID, PrevData and CurData here
    // but db.Statement.Dest or db.Statement.Model both contains data indifferent forms in different situation (eg, sometimes updated via map, sometimes via model itself). Also, incase of bulk inserts / conditional edits (eg, update product's discount where product category is 'xyz') the data is stored different in the db.Statement model.

    usernameInterface := db.Statement.Context.Value(constant.UserId)
    username, _ := usernameInterface.(string)

    auditLog := AuditLog{
        EntityClass:  entityName,
        EntityID:     "<id here>",
        Action:       "CREATE",
        ActionTaker:  username,
        PrevData:     "<json dump of prev data>",
        CurData:      "<json dump of cur data>",
        At:           time.Now(),
    }

    auditDB.Create(&auditLog)
}
go go-gorm hibernate-envers
1个回答
0
投票

我已经使用该回调函数创建了审核日志。 我在同一个数据库中创建了一个audit_log表,但它不起作用,然后我在单独的数据库中创建了一个audit_log表,它工作得很好

这里是代码示例

架构

type AuditLog struct {
    gorm.Modal

    UserID      uuid.UUID `json:"user_id"`
    UserEmail   string    `json:"user_email"`
    UserName    string    `json:"user_name"`
    PhcID       uuid.UUID `json:"phc_id"`
    Role        string    `json:"role"`
    EventName   string    `json:"event_name"`
    Action      string    `json:"action"`
    EntityID    string    `json:"entity_id"`
    EntityModel string    `json:"entity_model"`
    OldData     string    `json:"old_data"`
    NewData     string    `json:"new_data"`
    Diff        string    `json:"diff"`
    Domain      string    `json:"domain"`
}

此函数用于注册回调

func RegisterAuditLogCallbacks(db *gorm.DB) {
    db.Callback().Create().After("gorm:create").Register("audit_log_create", logCreate)
    db.Callback().Update().Before("gorm:update").Register("audit_log_update", logUpdate)

}

记录功能

创建

// logCreate callback
func logCreate(db *gorm.DB) {

    // Check if the audit log flag is set
    if db.Statement.Context.Value(auditLogFlagKey) != nil {
        return // Skip logging if already inside audit log creation
    }

    if db.Statement.Schema != nil {
        // Get the primary key value
        var entityID interface{}
        if db.Statement.Schema != nil && db.Statement.Schema.PrioritizedPrimaryField != nil {
            entityID, _ = db.Statement.Schema.PrioritizedPrimaryField.ValueOf(db.Statement.Context, db.Statement.ReflectValue)
        }

        if db.Statement.Table == "audit_logs" {
            return
        }

        userID := user_id
        role := user_role

        newData := make(map[string]interface{})
        newJSON, _ := json.Marshal(db.Statement.Model)
        json.Unmarshal(newJSON, &newData)

        //remove password from audit log ,I do this because encripted passwords can be expose :)
        delete(newData, "password")
        delete(newData, "Password")

        newJSON, err := json.Marshal(newData)
        if err != nil {
            logrus.Error("Error marshalling new data: ", err)
        }

        createAuditLogEntry(db, userID, role, "Create Event", "CREATE", db.Statement.Table, entityID, nil, newJSON)
    }
}

更新

func logUpdate(db *gorm.DB) {
    // Check if the audit log flag is set
    if db.Statement.Context.Value(auditLogFlagKey) != nil {
        return // Skip logging if already inside audit log creation
    }

    if db.Statement.Schema != nil {

        var entityID interface{}

        if db.Statement.Schema != nil && db.Statement.Schema.PrioritizedPrimaryField != nil {
            entityID, _ = db.Statement.Schema.PrioritizedPrimaryField.ValueOf(db.Statement.Context, db.Statement.ReflectValue)
        }

        if db.Statement.Table == "audit_logs" {
            return
        }

        userID := user_id
        role := user_role

        var oldData map[string]interface{}
        db.Session(&gorm.Session{NewDB: true}).Table(db.Statement.Table).Where("id = ?", entityID).Find(&oldData)
        newData := make(map[string]interface{})
        newJSON, _ := json.Marshal(db.Statement.Model)
        json.Unmarshal(newJSON, &newData)

        // Fetch the old data from the database

        //remove password from audit log
        delete(newData, "password")
        delete(newData, "Password")
        delete(oldData, "password")

        oldJSON, err := json.Marshal(oldData)
        if err != nil {
            logrus.Error("Error marshalling old data: ", err)
        }

        newJSON, err = json.Marshal(newData)
        if err != nil {
            logrus.Error("Error marshalling new data: ", err)
        }

        createAuditLogEntry(db, userID, role, "Update Event", "UPDATE", db.Statement.Table, entityID, oldJSON, newJSON)

    }

}

保存审核日志功能

const auditLogFlagKey = "audit_log"

// createAuditLogEntry creates a log entry in the AuditLog table.
func createAuditLogEntry(db *gorm.DB, userID uuid.UUID, role, eventName, action, entityModel string, entityID interface{}, oldData,
    newData []byte) {

    // Check if the flag is already set in the context, to avoid recursive logging
    if db.Statement.Context.Value(auditLogFlagKey) != nil {
        // If the flag is set, skip creating the audit log entry
        return
    }

    var diffJSON []byte

    if newData != nil {

        // Optionally, you can calculate the diff here
        diffJSON = calculateDiff(oldData, newData)
    }

    auditLog := AuditLog{
        UserID:      userID,
        Role:        role,
        EventName:   eventName,
        Action:      action,
        EntityID:    fmt.Sprintf("%v", entityID),
        EntityModel: entityModel,
        OldData:     string(oldData),
        NewData:     string(newData),
        Diff:        string(diffJSON),
        UserEmail:   user_email,
        UserName:    user_name,
        PhcID:       phc_id,
        Domain:      domain,
    }

    // Set the flag in the context to avoid infinite loop
    newCtx := context.WithValue(db.Statement.Context, auditLogFlagKey, true)
    db = db.WithContext(newCtx)

    // time.Sleep(5 * time.Second)

    // Create the audit log entry
    auditLogDB.Create(&auditLog)
}

我创建了这个额外的函数来标准化键并获取差异


func convertToSnakeCase(str string) string {
    // Compile a regex to find capital letters and replace with _<lowercase>
    var matchFirstCap = regexp.MustCompile("(.)([A-Z][a-z]+)")
    var matchAllCap = regexp.MustCompile("([a-z0-9])([A-Z])")

    // Insert underscore before the capital letters
    snake := matchFirstCap.ReplaceAllString(str, "${1}_${2}")
    snake = matchAllCap.ReplaceAllString(snake, "${1}_${2}")

    // Convert to lowercase
    return strings.ToLower(snake)
}

// NormalizeKeys converts all keys in a map to snake_case
func normalizeKeys(data map[string]interface{}) map[string]interface{} {
    normalized := make(map[string]interface{})
    for key, value := range data {
        // Convert each key to snake_case
        snakeKey := convertToSnakeCase(key)
        normalized[snakeKey] = value
    }
    return normalized
}

func calculateDiff(oldData, newData []byte) []byte {
    diffMap := make(map[string]interface{})

    oldMap := make(map[string]interface{})
    newMap := make(map[string]interface{})
    json.Unmarshal(oldData, &oldMap)
    json.Unmarshal(newData, &newMap)

    oldMap = normalizeKeys(oldMap)
    newMap = normalizeKeys(newMap)

    // fmt.Println("👉️ Old Map: ", oldMap)
    // fmt.Println("👉️ New Map: ", newMap)

    for key, value := range oldMap {
        if key == "password" {
            continue
        }
        if newMap[key] != value {
            diffMap[key] = map[string]interface{}{
                "old": value,
                "new": newMap[key],
            }
        }
    }

    diffJSON, _ := json.Marshal(diffMap)

    return diffJSON

}

© www.soinside.com 2019 - 2024. All rights reserved.