diff --git a/audit/audit.go b/audit/audit.go index 44d786ed2aa5a9b9874403a863044e2f8d32872a..1fb3c2c2ffb1c87127a6d9ab14607b85fa6161bd 100644 --- a/audit/audit.go +++ b/audit/audit.go @@ -3,25 +3,29 @@ package audit import ( "encoding/json" "fmt" + "github.com/r3labs/diff/v2" + "github.com/samber/lo" "gitlab.bob.co.za/bob-public-utils/bobgroup-go-utils/errors" "gitlab.bob.co.za/bob-public-utils/bobgroup-go-utils/logs" "gitlab.bob.co.za/bob-public-utils/bobgroup-go-utils/number_utils" + "gitlab.bob.co.za/bob-public-utils/bobgroup-go-utils/reflection" + "gitlab.bob.co.za/bob-public-utils/bobgroup-go-utils/string_utils" "reflect" "regexp" "strconv" "strings" - - "github.com/r3labs/diff/v2" - "gitlab.bob.co.za/bob-public-utils/bobgroup-go-utils/reflection" - "gitlab.bob.co.za/bob-public-utils/bobgroup-go-utils/string_utils" ) type FieldChange struct { - From interface{} `json:"change_from"` - To interface{} `json:"change_to"` + From any `json:"change_from"` + To any `json:"change_to"` } -func VerifyAuditEvents(original interface{}, new interface{}) error { +type IAuditFormatter interface { + FormatForAuditEvent() string +} + +func VerifyAuditEvents(original any, new any) error { if original != nil { structValue := reflect.ValueOf(original) if structValue.Kind() != reflect.Struct { @@ -39,12 +43,12 @@ func VerifyAuditEvents(original interface{}, new interface{}) error { return nil } -func GetChanges(original interface{}, new interface{}) (map[string]interface{}, error) { +func GetChanges(original any, new any) (map[string]any, error) { // Clean audit events original = cleanStruct(original) new = cleanStruct(new) - changes := map[string]interface{}{} + changes := map[string]any{} changelog, err := diff.Diff(original, new) if err != nil { return changes, err @@ -64,6 +68,11 @@ func GetChanges(original interface{}, new interface{}) (map[string]interface{}, // ["Account", "ID"] // 0 = Object // 1 = field + objectKey := ToSnakeCase(change.Path[0]) + didInsert := CheckToFormatForAuditEvent(changes, original, new, change.Path[0], objectKey, -1) + if didInsert { + continue + } ChildObjectChanges(changes, change.Path[0], change.Path[1], change.From, change.To) @@ -79,6 +88,11 @@ func GetChanges(original interface{}, new interface{}) (map[string]interface{}, if !string_utils.IsNumericString(indexString) { // Not an array, but a deeper nested object + didInsert := CheckToFormatForAuditEvent(changes, original, new, change.Path[0], objectKey, -1) + if didInsert { + continue + } + ChildObjectChanges(changes, change.Path[len(change.Path)-2], change.Path[len(change.Path)-1], change.From, change.To) continue } @@ -86,6 +100,11 @@ func GetChanges(original interface{}, new interface{}) (map[string]interface{}, index, _ := number_utils.StringToInt64(indexString) field := ToSnakeCase(change.Path[2]) + didInsert := CheckToFormatForAuditEvent(changes, original, new, change.Path[0], objectKey, int(index)) + if didInsert { + continue + } + if len(change.Path) == 5 && string_utils.IsNumericString(change.Path[3]) { // The field is actually an array of objects. field += fmt.Sprintf("[%s] (%s)", change.Path[3], ToSnakeCase(change.Path[4])) @@ -93,7 +112,7 @@ func GetChanges(original interface{}, new interface{}) (map[string]interface{}, arrayObject, present := changes[objectKey] if present { - if arrayOfObjects, ok := arrayObject.([]map[string]interface{}); ok { + if arrayOfObjects, ok := arrayObject.([]map[string]any); ok { arrayIndex := ArrayIndexForObjectIndex(arrayOfObjects, index) if arrayIndex != -1 { // Add field to existing object in array @@ -104,7 +123,7 @@ func GetChanges(original interface{}, new interface{}) (map[string]interface{}, } } else { // new object, append to existing array - fieldChange := map[string]interface{}{ + fieldChange := map[string]any{ "index": index, field: FieldChange{ From: change.From, @@ -117,14 +136,14 @@ func GetChanges(original interface{}, new interface{}) (map[string]interface{}, } } else { // Create array of objects - fieldChange := map[string]interface{}{ + fieldChange := map[string]any{ "index": index, field: FieldChange{ From: change.From, To: change.To, }, } - changes[objectKey] = []map[string]interface{}{ + changes[objectKey] = []map[string]any{ fieldChange, } } @@ -134,7 +153,7 @@ func GetChanges(original interface{}, new interface{}) (map[string]interface{}, return changes, nil } -func cleanStruct(object interface{}) interface{} { +func cleanStruct(object any) any { defer func() { if err := recover(); err != nil { logs.ErrorMsg(fmt.Sprintf("audit event panic: %+v", err)) @@ -175,9 +194,10 @@ func cleanStruct(object interface{}) interface{} { structField := val.Type().Field(i) // Determine whether the field should be included or excluded - value, _ := structField.Tag.Lookup("audit") - shouldIncludeForAudit := value == "true" - shouldExcludeForAudit := value == "false" + auditTag := structField.Tag.Get("audit") + auditTagOptions := strings.Split(auditTag, ",") + shouldIncludeForAudit := lo.Contains(auditTagOptions, "true") + shouldExcludeForAudit := lo.Contains(auditTagOptions, "false") // If the audit tag is present and specified to 'true', we should always include the relation if shouldIncludeForAudit { @@ -197,14 +217,158 @@ func cleanStruct(object interface{}) interface{} { return object } -func ChildObjectChanges(changes map[string]interface{}, objectPath string, fieldPath string, changeFrom interface{}, changeTo interface{}) { +func CheckToFormatForAuditEvent(changes map[string]any, original any, new any, field string, objectKey string, index int) (didInsert bool) { + originalStructField, originalFieldValue, found := getFieldFromStruct(original, field) + if !found { + return false + } + _, newFieldValue, found := getFieldFromStruct(new, field) + if !found { + return false + } + doGroupSlice := doGroupSliceForAuditEvent(originalStructField) + + originalFormattedObject := checkToFormatForAuditEvent(originalFieldValue, index) + newFormattedObject := checkToFormatForAuditEvent(newFieldValue, index) + if originalFormattedObject != nil || + newFormattedObject != nil { + if _, present := changes[objectKey]; present { + if doGroupSlice { + // The object has already been added to the changes - group the new change with the previous + existingChanges, ok := changes[objectKey].(FieldChange) + if !ok { + return true + } + + if originalFormattedObject != nil { + existingChanges.From = appendNewChangeToExistingChanges(existingChanges.From, *originalFormattedObject) + } + + if newFormattedObject != nil { + existingChanges.To = appendNewChangeToExistingChanges(existingChanges.To, *newFormattedObject) + } + changes[objectKey] = existingChanges + } + + return true + } + + if doGroupSlice { + changes[objectKey] = FieldChange{ + From: []string{*originalFormattedObject}, + To: []string{*newFormattedObject}, + } + } else { + if index > -1 { + objectKey = fmt.Sprintf("%s[%d]", field, index) + } + + changes[objectKey] = FieldChange{ + From: originalFormattedObject, + To: newFormattedObject, + } + } + + return true + } + + return false +} + +func appendNewChangeToExistingChanges(existingChanges any, newChange string) any { + existingChangesStrings, ok := existingChanges.([]string) + if !ok { + return existingChanges + } + + if !lo.Contains(existingChangesStrings, newChange) { + existingChangesStrings = append(existingChangesStrings, newChange) + } + + return existingChangesStrings +} + +func getFieldFromStruct(object any, field string) (reflect.StructField, reflect.Value, bool) { + objectValue := reflect.ValueOf(object) + if objectValue.Kind() == reflect.Ptr { + objectValue = objectValue.Elem() + } + + if objectValue.Kind() != reflect.Struct { + // Fields can only be retrieved from structs + return reflect.StructField{}, reflect.Value{}, false + } + + objectStructField, found := objectValue.Type().FieldByName(field) + if !found { + return reflect.StructField{}, reflect.Value{}, false + } + + objectFieldValue := objectValue.FieldByName(field) + + return objectStructField, objectFieldValue, true +} + +func doGroupSliceForAuditEvent(objectField reflect.StructField) bool { + auditTag, found := objectField.Tag.Lookup("audit") + if found { + auditTagOptions := strings.Split(auditTag, ",") + if lo.Contains(auditTagOptions, "group") { + return true + } + } + + return false +} + +func checkToExecuteAuditFormatter(fieldValue reflect.Value) *string { + if auditFormatterObject, ok := fieldValue.Interface().(IAuditFormatter); ok { + fieldString := auditFormatterObject.FormatForAuditEvent() + if fieldString != "" { + return &fieldString + } + } + return nil +} + +func checkToFormatForAuditEvent(fieldValue reflect.Value, index int) *string { + if fieldValue.Kind() == reflect.Ptr { + fieldValue = fieldValue.Elem() + } + + var formattedField *string + if fieldValue.Kind() == reflect.Struct { + formattedField = checkToExecuteAuditFormatter(fieldValue) + } else if fieldValue.Kind() == reflect.Slice && + fieldValue.Len() > 0 { + if index >= fieldValue.Len() { + return nil + } + + sliceFieldValue := fieldValue.Index(index) + if sliceFieldValue.Kind() == reflect.Ptr { + sliceFieldValue = sliceFieldValue.Elem() + } + + if sliceFieldValue.Kind() != reflect.Struct { + // Not a slice of structs - ignore the format flag + return nil + } + + formattedField = checkToExecuteAuditFormatter(sliceFieldValue) + } + + return formattedField +} + +func ChildObjectChanges(changes map[string]any, objectPath string, fieldPath string, changeFrom any, changeTo any) { objectKey := ToSnakeCase(objectPath) field := ToSnakeCase(fieldPath) existingObject, present := changes[objectKey] if present { - if object, ok := existingObject.(map[string]interface{}); ok { + if object, ok := existingObject.(map[string]any); ok { object[field] = FieldChange{ From: changeFrom, To: changeTo, @@ -212,7 +376,7 @@ func ChildObjectChanges(changes map[string]interface{}, objectPath string, field changes[objectKey] = object } } else { - fieldChange := map[string]interface{}{ + fieldChange := map[string]any{ field: FieldChange{ From: changeFrom, To: changeTo, @@ -223,7 +387,7 @@ func ChildObjectChanges(changes map[string]interface{}, objectPath string, field } // ArrayIndexForObjectIndex gets the index of arrayOfObjects where the object's index field is equal to objectIndex. -func ArrayIndexForObjectIndex(arrayOfObjects []map[string]interface{}, objectIndex int64) int { +func ArrayIndexForObjectIndex(arrayOfObjects []map[string]any, objectIndex int64) int { for arrayIndex, object := range arrayOfObjects { index, present := object["index"] if present { @@ -238,8 +402,8 @@ func ArrayIndexForObjectIndex(arrayOfObjects []map[string]interface{}, objectInd // GetAllChanges Returns the diff, structured in json, recursively // Be warned, here be dragons. Debug this first to understand how it works -func GetAllChanges(original interface{}, new interface{}) (map[string]interface{}, error) { - changes := map[string]interface{}{} +func GetAllChanges(original any, new any) (map[string]any, error) { + changes := map[string]any{} changelog, err := diff.Diff(original, new) if err != nil { return changes, err @@ -402,7 +566,7 @@ func ToSnakeCase(str string) string { return strings.ToLower(snake) } -func GetIntValue(object interface{}, key string) int64 { +func GetIntValue(object any, key string) int64 { structValue := reflect.ValueOf(object) if structValue.Kind() == reflect.Struct { field := structValue.FieldByName(key) @@ -412,7 +576,7 @@ func GetIntValue(object interface{}, key string) int64 { return 0 } -func GetStringValue(object interface{}, key string) string { +func GetStringValue(object any, key string) string { structValue := reflect.ValueOf(object) if structValue.Kind() == reflect.Struct { field := structValue.FieldByName(key)