-
Notifications
You must be signed in to change notification settings - Fork 894
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
Changes from 9 commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
bdde128
chore: rearrange audit logging code into enterprise folder
coadler 823a58d
add nop auditor
coadler b3776e0
fixup! add nop auditor
coadler d23edf8
Merge branch 'main' into colin/auditv2
coadler b417218
typo
coadler 1188f69
fix tests
coadler 595f4c1
fixup! fix tests
coadler 8f915f6
add auditor constructor
coadler 10a0d3b
spike comments
coadler ec83dfc
fixup! spike comments
coadler f7d579c
Merge branch 'main' into colin/auditv2
coadler 9cfcbeb
fixup! Merge branch 'main' into colin/auditv2
coadler c3115ec
comment why Differ is necessary
coadler File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
|
||
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{} } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 { | ||
coadler marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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) | ||
} |
This file was deleted.
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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