Skip to content

chore: rearrange audit logging code into enterprise folder #3741

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
Aug 31, 2022
24 changes: 24 additions & 0 deletions coderd/audit/audit.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package audit

import (
"context"

"github.com/coder/coder/coderd/database"
)

type Auditor interface {
Export(ctx context.Context, alog database.AuditLog) error
diff(old, new any) Map
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One missing item is the "noop" implementation of the interface


func NewNop() Auditor {
return nop{}
}

type nop struct{}

func (nop) Export(context.Context, database.AuditLog) error {
return nil
}

func (nop) diff(any, any) Map { return Map{} }
184 changes: 31 additions & 153 deletions coderd/audit/diff.go
Original file line number Diff line number Diff line change
@@ -1,170 +1,48 @@
package audit

import (
"database/sql"
"fmt"
"reflect"

"github.com/google/uuid"
"github.com/coder/coder/coderd/database"
)

// TODO: this might need to be in the database package.
type Map map[string]interface{}
// Auditable is mostly a marker interface. It contains a definitive list of all
// auditable types. If you want to audit a new type, first define it in
// AuditableResources, then add it to this interface.
type Auditable interface {
database.APIKey |
database.Organization |
database.OrganizationMember |
database.Template |
database.TemplateVersion |
database.User |
database.Workspace |
database.GitSSHKey
}

// Map is a map of changed fields in an audited resource. It maps field names to
// the old and new value for that field.
type Map map[string]OldNew

// OldNew is a pair of values representing the old value and the new value.
type OldNew struct {
Old any
New any
Secret bool
}

// Empty returns a default value of type T.
func Empty[T Auditable]() T {
var t T
return t
}

// Diff compares two auditable resources and produces a Map of the changed
// values.
func Diff[T Auditable](left, right T) Map {
// Values are equal, return an empty diff.
if reflect.DeepEqual(left, right) {
return Map{}
}

return diffValues(left, right, AuditableResources)
}

func structName(t reflect.Type) string {
return t.PkgPath() + "." + t.Name()
}

func diffValues[T any](left, right T, table Table) Map {
var (
baseDiff = Map{}

leftV = reflect.ValueOf(left)

rightV = reflect.ValueOf(right)
rightT = reflect.TypeOf(right)

diffKey = table[structName(rightT)]
)

if diffKey == nil {
panic(fmt.Sprintf("dev error: type %q (type %T) attempted audit but not auditable", rightT.Name(), right))
}

for i := 0; i < rightT.NumField(); i++ {
var (
leftF = leftV.Field(i)
rightF = rightV.Field(i)

leftI = leftF.Interface()
rightI = rightF.Interface()

diffName = rightT.Field(i).Tag.Get("json")
)

atype, ok := diffKey[diffName]
if !ok {
panic(fmt.Sprintf("dev error: field %q lacks audit information", diffName))
}

if atype == ActionIgnore {
continue
}

// coerce struct types that would produce bad diffs.
if leftI, rightI, ok = convertDiffType(leftI, rightI); ok {
leftF, rightF = reflect.ValueOf(leftI), reflect.ValueOf(rightI)
}

// If the field is a pointer, dereference it. Nil pointers are coerced
// to the zero value of their underlying type.
if leftF.Kind() == reflect.Ptr && rightF.Kind() == reflect.Ptr {
leftF, rightF = derefPointer(leftF), derefPointer(rightF)
leftI, rightI = leftF.Interface(), rightF.Interface()
}

// Recursively walk up nested structs.
if rightF.Kind() == reflect.Struct {
baseDiff[diffName] = diffValues(leftI, rightI, table)
continue
}

if !reflect.DeepEqual(leftI, rightI) {
switch atype {
case ActionTrack:
baseDiff[diffName] = rightI
case ActionSecret:
baseDiff[diffName] = reflect.Zero(rightF.Type()).Interface()
}
}
}

return baseDiff
}

// convertDiffType converts external struct types to primitive types.
//
//nolint:forcetypeassert
func convertDiffType(left, right any) (newLeft, newRight any, changed bool) {
switch typed := left.(type) {
case uuid.UUID:
return typed.String(), right.(uuid.UUID).String(), true

case uuid.NullUUID:
leftStr, _ := typed.MarshalText()
rightStr, _ := right.(uuid.NullUUID).MarshalText()
return string(leftStr), string(rightStr), true

case sql.NullString:
leftStr := typed.String
if !typed.Valid {
leftStr = "null"
}

rightStr := right.(sql.NullString).String
if !right.(sql.NullString).Valid {
rightStr = "null"
}

return leftStr, rightStr, true

case sql.NullInt64:
var leftInt64Ptr *int64
var rightInt64Ptr *int64
if !typed.Valid {
leftInt64Ptr = nil
} else {
leftInt64Ptr = ptr(typed.Int64)
}

rightInt64Ptr = ptr(right.(sql.NullInt64).Int64)
if !right.(sql.NullInt64).Valid {
rightInt64Ptr = nil
}

return leftInt64Ptr, rightInt64Ptr, true

default:
return left, right, false
}
}

// derefPointer deferences a reflect.Value that is a pointer to its underlying
// value. It dereferences recursively until it finds a non-pointer value. If the
// pointer is nil, it will be coerced to the zero value of the underlying type.
func derefPointer(ptr reflect.Value) reflect.Value {
if !ptr.IsNil() {
// Grab the value the pointer references.
ptr = ptr.Elem()
} else {
// Coerce nil ptrs to zero'd values of their underlying type.
ptr = reflect.Zero(ptr.Type().Elem())
}

// Recursively deref nested pointers.
if ptr.Kind() == reflect.Ptr {
return derefPointer(ptr)
}
func Diff[T Auditable](a Auditor, left, right T) Map { return a.diff(left, right) }

return ptr
type Differ struct {
DiffFn func(old, new any) Map
}

func ptr[T any](x T) *T {
return &x
func (d Differ) diff(old, new any) Map {
return d.DiffFn(old, new)
}
163 changes: 0 additions & 163 deletions coderd/audit/diff_internal_test.go

This file was deleted.

Loading