From bdde12847b08af9995ce6c89975407fb31df6cc5 Mon Sep 17 00:00:00 2001 From: Colin Adler Date: Thu, 18 Aug 2022 14:40:36 -0500 Subject: [PATCH 01/11] chore: rearrange audit logging code into enterprise folder --- coderd/audit.go | 1 + coderd/audit/audit.go | 12 ++ coderd/audit/diff.go | 184 +++-------------- coderd/audit/diff_internal_test.go | 163 --------------- coderd/audit/request.go | 52 +++++ coderd/database/dump.sql | 4 +- .../000039_audit_addtl_fields.down.sql | 3 + .../000039_audit_addtl_fields.up.sql | 4 + coderd/database/models.go | 26 +-- coderd/database/queries.sql.go | 40 ++-- coderd/database/queries/auditlogs.sql | 6 +- codersdk/audit.go | 52 +++++ enterprise/audit/audit.go | 34 +++ enterprise/audit/diff.go | 156 ++++++++++++++ .../audit/diff_internal_test.go | 194 ++++++++++++++++-- enterprise/audit/filter.go | 42 ++++ {coderd => enterprise}/audit/table.go | 13 -- enterprise/coderd/licenses.go | 1 - site/src/api/typesGenerated.ts | 33 +++ 19 files changed, 636 insertions(+), 384 deletions(-) create mode 100644 coderd/audit.go create mode 100644 coderd/audit/audit.go delete mode 100644 coderd/audit/diff_internal_test.go create mode 100644 coderd/audit/request.go create mode 100644 coderd/database/migrations/000039_audit_addtl_fields.down.sql create mode 100644 coderd/database/migrations/000039_audit_addtl_fields.up.sql create mode 100644 codersdk/audit.go create mode 100644 enterprise/audit/audit.go create mode 100644 enterprise/audit/diff.go rename coderd/audit/diff_test.go => enterprise/audit/diff_internal_test.go (63%) create mode 100644 enterprise/audit/filter.go rename {coderd => enterprise}/audit/table.go (91%) diff --git a/coderd/audit.go b/coderd/audit.go new file mode 100644 index 0000000000000..ddb2b8b672186 --- /dev/null +++ b/coderd/audit.go @@ -0,0 +1 @@ +package coderd diff --git a/coderd/audit/audit.go b/coderd/audit/audit.go new file mode 100644 index 0000000000000..19086a86f408d --- /dev/null +++ b/coderd/audit/audit.go @@ -0,0 +1,12 @@ +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 +} diff --git a/coderd/audit/diff.go b/coderd/audit/diff.go index efb8d87548f86..a9607115efc60 100644 --- a/coderd/audit/diff.go +++ b/coderd/audit/diff.go @@ -1,16 +1,35 @@ 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. `any` can be a +// map[string]any in the case of nested structs, or an OldNew struct +// representing a changed value. +type Map map[string]any +// OldNew is a pair of values representing the old value and the new value. +type OldNew struct { + Old any + New any +} + +// Empty returns a default value of type T. func Empty[T Auditable]() T { var t T return t @@ -18,153 +37,4 @@ func Empty[T Auditable]() 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) - } - - return ptr -} - -func ptr[T any](x T) *T { - return &x -} +func Diff[T Auditable](a Auditor, left, right T) Map { return a.diff(left, right) } diff --git a/coderd/audit/diff_internal_test.go b/coderd/audit/diff_internal_test.go deleted file mode 100644 index 88d477a727972..0000000000000 --- a/coderd/audit/diff_internal_test.go +++ /dev/null @@ -1,163 +0,0 @@ -package audit - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "k8s.io/utils/pointer" -) - -func Test_diffValues(t *testing.T) { - t.Parallel() - - t.Run("Normal", func(t *testing.T) { - t.Parallel() - - type foo struct { - Bar string `json:"bar"` - Baz int64 `json:"baz"` - } - - table := auditMap(map[any]map[string]Action{ - &foo{}: { - "bar": ActionTrack, - "baz": ActionTrack, - }, - }) - - runDiffTests(t, table, []diffTest{ - { - name: "LeftEmpty", - left: foo{Bar: "", Baz: 0}, right: foo{Bar: "bar", Baz: 10}, - exp: Map{ - "bar": "bar", - "baz": int64(10), - }, - }, - { - name: "RightEmpty", - left: foo{Bar: "Bar", Baz: 10}, right: foo{Bar: "", Baz: 0}, - exp: Map{ - "bar": "", - "baz": int64(0), - }, - }, - { - name: "NoChange", - left: foo{Bar: "", Baz: 0}, right: foo{Bar: "", Baz: 0}, - exp: Map{}, - }, - { - name: "SingleFieldChange", - left: foo{Bar: "", Baz: 0}, right: foo{Bar: "Bar", Baz: 0}, - exp: Map{ - "bar": "Bar", - }, - }, - }) - }) - - t.Run("PointerField", func(t *testing.T) { - t.Parallel() - - type foo struct { - Bar *string `json:"bar"` - } - - table := auditMap(map[any]map[string]Action{ - &foo{}: { - "bar": ActionTrack, - }, - }) - - runDiffTests(t, table, []diffTest{ - { - name: "LeftNil", - left: foo{Bar: nil}, right: foo{Bar: pointer.StringPtr("baz")}, - exp: Map{"bar": "baz"}, - }, - { - name: "RightNil", - left: foo{Bar: pointer.StringPtr("baz")}, right: foo{Bar: nil}, - exp: Map{"bar": ""}, - }, - }) - }) - - t.Run("NestedStruct", func(t *testing.T) { - t.Parallel() - - type bar struct { - Baz string `json:"baz"` - } - - type foo struct { - Bar *bar `json:"bar"` - } - - table := auditMap(map[any]map[string]Action{ - &foo{}: { - "bar": ActionTrack, - }, - &bar{}: { - "baz": ActionTrack, - }, - }) - - runDiffTests(t, table, []diffTest{ - { - name: "LeftEmpty", - left: foo{Bar: &bar{}}, right: foo{Bar: &bar{Baz: "baz"}}, - exp: Map{ - "bar": Map{ - "baz": "baz", - }, - }, - }, - { - name: "RightEmpty", - left: foo{Bar: &bar{Baz: "baz"}}, right: foo{Bar: &bar{}}, - exp: Map{ - "bar": Map{ - "baz": "", - }, - }, - }, - { - name: "LeftNil", - left: foo{Bar: nil}, right: foo{Bar: &bar{}}, - exp: Map{ - "bar": Map{}, - }, - }, - { - name: "RightNil", - left: foo{Bar: &bar{Baz: "baz"}}, right: foo{Bar: nil}, - exp: Map{ - "bar": Map{ - "baz": "", - }, - }, - }, - }) - }) -} - -type diffTest struct { - name string - left, right any - exp any -} - -func runDiffTests(t *testing.T, table Table, tests []diffTest) { - t.Helper() - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - assert.Equal(t, - test.exp, - diffValues(test.left, test.right, table), - ) - }) - } -} diff --git a/coderd/audit/request.go b/coderd/audit/request.go new file mode 100644 index 0000000000000..6f15db05ab4ef --- /dev/null +++ b/coderd/audit/request.go @@ -0,0 +1,52 @@ +package audit + +import ( + "context" + "net/http" + + chimw "github.com/go-chi/chi/v5/middleware" + "github.com/google/uuid" + + "cdr.dev/slog" + "github.com/coder/coder/coderd/database" +) + +type RequstParams struct { + Audit Auditor + Log slog.Logger + + Action database.AuditAction + ResourceType database.ResourceType + Actor uuid.UUID +} + +type Request[T Auditable] struct { + params *RequstParams + + Old T + New T +} + +// InitRequest initializes an audit log for a request. It returns a function +// that should be deferred, causing the audit log to be committed when the +// handler returns. +func InitRequest[T Auditable](w http.ResponseWriter, p *RequstParams) (*Request[T], func()) { + sw, ok := w.(chimw.WrapResponseWriter) + if !ok { + panic("dev error: http.ResponseWriter is not chimw.WrapResponseWriter") + } + + req := &Request[T]{ + params: p, + } + + return req, func() { + ctx := context.Background() + code := sw.Status() + + err := p.Audit.Export(ctx, database.AuditLog{StatusCode: int32(code)}) + if err != nil { + p.Log.Error(ctx, "export audit log", slog.Error(err)) + } + } +} diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index 9e28ed5bc4c48..13de9addc0e9a 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -112,7 +112,9 @@ CREATE TABLE audit_logs ( resource_target text NOT NULL, action audit_action NOT NULL, diff jsonb NOT NULL, - status_code integer NOT NULL + status_code integer NOT NULL, + additional_fields jsonb NOT NULL, + request_id uuid NOT NULL ); CREATE TABLE files ( diff --git a/coderd/database/migrations/000039_audit_addtl_fields.down.sql b/coderd/database/migrations/000039_audit_addtl_fields.down.sql new file mode 100644 index 0000000000000..843aa19f6f4b2 --- /dev/null +++ b/coderd/database/migrations/000039_audit_addtl_fields.down.sql @@ -0,0 +1,3 @@ +ALTER TABLE audit_logs + DROP COLUMN additional_fields, + DROP COLUMN request_id; diff --git a/coderd/database/migrations/000039_audit_addtl_fields.up.sql b/coderd/database/migrations/000039_audit_addtl_fields.up.sql new file mode 100644 index 0000000000000..dc51bb9075dd9 --- /dev/null +++ b/coderd/database/migrations/000039_audit_addtl_fields.up.sql @@ -0,0 +1,4 @@ +ALTER TABLE audit_logs ADD COLUMN additional_fields jsonb NOT NULL DEFAULT '{}'::jsonb; +ALTER TABLE audit_logs ALTER COLUMN additional_fields DROP DEFAULT; +ALTER TABLE audit_logs ADD COLUMN request_id uuid NOT NULL DEFAULT '00000000-0000-0000-0000-000000000000'::uuid; +ALTER TABLE audit_logs ALTER COLUMN request_id DROP DEFAULT; diff --git a/coderd/database/models.go b/coderd/database/models.go index f9f559711ec32..ad7f7a151bf60 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -326,18 +326,20 @@ type APIKey struct { } type AuditLog struct { - ID uuid.UUID `db:"id" json:"id"` - Time time.Time `db:"time" json:"time"` - UserID uuid.UUID `db:"user_id" json:"user_id"` - OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"` - Ip pqtype.Inet `db:"ip" json:"ip"` - UserAgent string `db:"user_agent" json:"user_agent"` - ResourceType ResourceType `db:"resource_type" json:"resource_type"` - ResourceID uuid.UUID `db:"resource_id" json:"resource_id"` - ResourceTarget string `db:"resource_target" json:"resource_target"` - Action AuditAction `db:"action" json:"action"` - Diff json.RawMessage `db:"diff" json:"diff"` - StatusCode int32 `db:"status_code" json:"status_code"` + ID uuid.UUID `db:"id" json:"id"` + Time time.Time `db:"time" json:"time"` + UserID uuid.UUID `db:"user_id" json:"user_id"` + OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"` + Ip pqtype.Inet `db:"ip" json:"ip"` + UserAgent string `db:"user_agent" json:"user_agent"` + ResourceType ResourceType `db:"resource_type" json:"resource_type"` + ResourceID uuid.UUID `db:"resource_id" json:"resource_id"` + ResourceTarget string `db:"resource_target" json:"resource_target"` + Action AuditAction `db:"action" json:"action"` + Diff json.RawMessage `db:"diff" json:"diff"` + StatusCode int32 `db:"status_code" json:"status_code"` + AdditionalFields json.RawMessage `db:"additional_fields" json:"additional_fields"` + RequestID uuid.UUID `db:"request_id" json:"request_id"` } type File struct { diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 9a22d86d888a8..00e854414cbf9 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -192,7 +192,7 @@ func (q *sqlQuerier) UpdateAPIKeyByID(ctx context.Context, arg UpdateAPIKeyByIDP const getAuditLogsBefore = `-- name: GetAuditLogsBefore :many SELECT - id, time, user_id, organization_id, ip, user_agent, resource_type, resource_id, resource_target, action, diff, status_code + id, time, user_id, organization_id, ip, user_agent, resource_type, resource_id, resource_target, action, diff, status_code, additional_fields, request_id FROM audit_logs WHERE @@ -233,6 +233,8 @@ func (q *sqlQuerier) GetAuditLogsBefore(ctx context.Context, arg GetAuditLogsBef &i.Action, &i.Diff, &i.StatusCode, + &i.AdditionalFields, + &i.RequestID, ); err != nil { return nil, err } @@ -261,25 +263,29 @@ INSERT INTO resource_target, action, diff, - status_code + status_code, + additional_fields, + request_id ) VALUES - ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) RETURNING id, time, user_id, organization_id, ip, user_agent, resource_type, resource_id, resource_target, action, diff, status_code + ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14) RETURNING id, time, user_id, organization_id, ip, user_agent, resource_type, resource_id, resource_target, action, diff, status_code, additional_fields, request_id ` type InsertAuditLogParams struct { - ID uuid.UUID `db:"id" json:"id"` - Time time.Time `db:"time" json:"time"` - UserID uuid.UUID `db:"user_id" json:"user_id"` - OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"` - Ip pqtype.Inet `db:"ip" json:"ip"` - UserAgent string `db:"user_agent" json:"user_agent"` - ResourceType ResourceType `db:"resource_type" json:"resource_type"` - ResourceID uuid.UUID `db:"resource_id" json:"resource_id"` - ResourceTarget string `db:"resource_target" json:"resource_target"` - Action AuditAction `db:"action" json:"action"` - Diff json.RawMessage `db:"diff" json:"diff"` - StatusCode int32 `db:"status_code" json:"status_code"` + ID uuid.UUID `db:"id" json:"id"` + Time time.Time `db:"time" json:"time"` + UserID uuid.UUID `db:"user_id" json:"user_id"` + OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"` + Ip pqtype.Inet `db:"ip" json:"ip"` + UserAgent string `db:"user_agent" json:"user_agent"` + ResourceType ResourceType `db:"resource_type" json:"resource_type"` + ResourceID uuid.UUID `db:"resource_id" json:"resource_id"` + ResourceTarget string `db:"resource_target" json:"resource_target"` + Action AuditAction `db:"action" json:"action"` + Diff json.RawMessage `db:"diff" json:"diff"` + StatusCode int32 `db:"status_code" json:"status_code"` + AdditionalFields json.RawMessage `db:"additional_fields" json:"additional_fields"` + RequestID uuid.UUID `db:"request_id" json:"request_id"` } func (q *sqlQuerier) InsertAuditLog(ctx context.Context, arg InsertAuditLogParams) (AuditLog, error) { @@ -296,6 +302,8 @@ func (q *sqlQuerier) InsertAuditLog(ctx context.Context, arg InsertAuditLogParam arg.Action, arg.Diff, arg.StatusCode, + arg.AdditionalFields, + arg.RequestID, ) var i AuditLog err := row.Scan( @@ -311,6 +319,8 @@ func (q *sqlQuerier) InsertAuditLog(ctx context.Context, arg InsertAuditLogParam &i.Action, &i.Diff, &i.StatusCode, + &i.AdditionalFields, + &i.RequestID, ) return i, err } diff --git a/coderd/database/queries/auditlogs.sql b/coderd/database/queries/auditlogs.sql index e827ea1cd113f..d9f80a0967b18 100644 --- a/coderd/database/queries/auditlogs.sql +++ b/coderd/database/queries/auditlogs.sql @@ -26,7 +26,9 @@ INSERT INTO resource_target, action, diff, - status_code + status_code, + additional_fields, + request_id ) VALUES - ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) RETURNING *; + ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14) RETURNING *; diff --git a/codersdk/audit.go b/codersdk/audit.go new file mode 100644 index 0000000000000..19d3ec6f81af3 --- /dev/null +++ b/codersdk/audit.go @@ -0,0 +1,52 @@ +package codersdk + +import ( + "encoding/json" + "net/netip" + "time" + + "github.com/google/uuid" +) + +type ResourceType string + +const ( + ResourceTypeOrganization ResourceType = "organization" + ResourceTypeTemplate ResourceType = "template" + ResourceTypeTemplateVersion ResourceType = "template_version" + ResourceTypeUser ResourceType = "user" + ResourceTypeWorkspace ResourceType = "workspace" +) + +type AuditAction string + +const ( + AuditActionCreate AuditAction = "create" + AuditActionWrite AuditAction = "write" + AuditActionDelete AuditAction = "delete" +) + +type AuditDiff map[string]struct { + Old any + New any +} + +type AuditLog struct { + ID uuid.UUID `json:"id"` + RequestID uuid.UUID `json:"request_id"` + Time time.Time `json:"time"` + OrganizationID uuid.UUID `json:"organization_id"` + IP netip.Addr `json:"ip"` + UserAgent string `json:"user_agent"` + ResourceType ResourceType `json:"resource_type"` + ResourceID uuid.UUID `json:"resource_id"` + ResourceTarget string `json:"resource_target"` + Action AuditAction `json:"action"` + Diff AuditDiff `json:"diff"` + StatusCode int32 `json:"status_code"` + AdditionalFields json.RawMessage `json:"additional_fields"` + Description string `json:"description"` + + User *User `json:"user"` + Resource json.RawMessage `json:"resource"` +} diff --git a/enterprise/audit/audit.go b/enterprise/audit/audit.go new file mode 100644 index 0000000000000..cb1ffa4ab751a --- /dev/null +++ b/enterprise/audit/audit.go @@ -0,0 +1,34 @@ +package audit + +import ( + "context" + + "github.com/coder/coder/coderd/audit" + "github.com/coder/coder/coderd/database" +) + +// Backends can store or send audit logs to arbitrary locations. +type Backend interface { + // Decision determines the FilterDecisions that the backend tolerates. + Decision() FilterDecision + // Export sends an audit log to the backend. + Export(ctx context.Context, alog database.AuditLog) error +} + +// auditor is the enterprise impelentation of the Auditor interface. +type auditor struct { + //nolint:unused + filter Filter + //nolint:unused + backends []Backend +} + +//nolint:unused +func (*auditor) Export(context.Context, database.AuditLog) error { + panic("not implemented") // TODO: Implement +} + +//nolint:unused +func (*auditor) diff(any, any) audit.Map { + panic("not implemented") // TODO: Implement +} diff --git a/enterprise/audit/diff.go b/enterprise/audit/diff.go new file mode 100644 index 0000000000000..66d3e1cee46b9 --- /dev/null +++ b/enterprise/audit/diff.go @@ -0,0 +1,156 @@ +package audit + +import ( + "database/sql" + "fmt" + "reflect" + + "github.com/google/uuid" + + "github.com/coder/coder/coderd/audit" +) + +func structName(t reflect.Type) string { + return t.PkgPath() + "." + t.Name() +} + +func diffValues[T any](left, right T, table Table) audit.Map { + var ( + baseDiff = audit.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] = audit.OldNew{Old: leftI, New: rightI} + case ActionSecret: + baseDiff[diffName] = audit.OldNew{ + Old: reflect.Zero(rightF.Type()).Interface(), + New: 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) + } + + return ptr +} + +func ptr[T any](x T) *T { + return &x +} diff --git a/coderd/audit/diff_test.go b/enterprise/audit/diff_internal_test.go similarity index 63% rename from coderd/audit/diff_test.go rename to enterprise/audit/diff_internal_test.go index 0e90d8c30dcad..0c7526a8050f3 100644 --- a/coderd/audit/diff_test.go +++ b/enterprise/audit/diff_internal_test.go @@ -1,4 +1,4 @@ -package audit_test +package audit import ( "database/sql" @@ -7,16 +7,177 @@ import ( "time" "github.com/google/uuid" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "k8s.io/utils/pointer" "github.com/coder/coder/coderd/audit" "github.com/coder/coder/coderd/database" ) -func TestDiff(t *testing.T) { +func Test_diffValues(t *testing.T) { t.Parallel() - runDiffTests(t, []diffTest[database.GitSSHKey]{ + t.Run("Normal", func(t *testing.T) { + t.Parallel() + + type foo struct { + Bar string `json:"bar"` + Baz int64 `json:"baz"` + } + + table := auditMap(map[any]map[string]Action{ + &foo{}: { + "bar": ActionTrack, + "baz": ActionTrack, + }, + }) + + runDiffValuesTests(t, table, []diffTest{ + { + name: "LeftEmpty", + left: foo{Bar: "", Baz: 0}, right: foo{Bar: "bar", Baz: 10}, + exp: audit.Map{ + "bar": audit.OldNew{Old: "", New: "bar"}, + "baz": audit.OldNew{Old: 0, New: 10}, + }, + }, + { + name: "RightEmpty", + left: foo{Bar: "Bar", Baz: 10}, right: foo{Bar: "", Baz: 0}, + exp: audit.Map{ + "bar": audit.OldNew{Old: "Bar", New: ""}, + "baz": audit.OldNew{Old: 10, New: 0}, + }, + }, + { + name: "NoChange", + left: foo{Bar: "", Baz: 0}, right: foo{Bar: "", Baz: 0}, + exp: audit.Map{}, + }, + { + name: "SingleFieldChange", + left: foo{Bar: "", Baz: 0}, right: foo{Bar: "Bar", Baz: 0}, + exp: audit.Map{ + "bar": audit.OldNew{Old: "", New: "Bar"}, + }, + }, + }) + }) + + t.Run("PointerField", func(t *testing.T) { + t.Parallel() + + type foo struct { + Bar *string `json:"bar"` + } + + table := auditMap(map[any]map[string]Action{ + &foo{}: { + "bar": ActionTrack, + }, + }) + + runDiffValuesTests(t, table, []diffTest{ + { + name: "LeftNil", + left: foo{Bar: nil}, right: foo{Bar: pointer.StringPtr("baz")}, + exp: audit.Map{ + "bar": audit.OldNew{Old: "", New: "baz"}, + }, + }, + { + name: "RightNil", + left: foo{Bar: pointer.StringPtr("baz")}, right: foo{Bar: nil}, + exp: audit.Map{ + "bar": audit.OldNew{Old: "baz", New: ""}, + }, + }, + }) + }) + + t.Run("NestedStruct", func(t *testing.T) { + t.Parallel() + + type bar struct { + Baz string `json:"baz"` + } + + type foo struct { + Bar *bar `json:"bar"` + } + + table := auditMap(map[any]map[string]Action{ + &foo{}: { + "bar": ActionTrack, + }, + &bar{}: { + "baz": ActionTrack, + }, + }) + + runDiffValuesTests(t, table, []diffTest{ + { + name: "LeftEmpty", + left: foo{Bar: &bar{}}, right: foo{Bar: &bar{Baz: "baz"}}, + exp: audit.Map{ + "bar": audit.Map{ + "baz": audit.OldNew{Old: "", New: "baz"}, + }, + }, + }, + { + name: "RightEmpty", + left: foo{Bar: &bar{Baz: "baz"}}, right: foo{Bar: &bar{}}, + exp: audit.Map{ + "bar": audit.Map{ + "baz": audit.OldNew{Old: "baz", New: ""}, + }, + }, + }, + { + name: "LeftNil", + left: foo{Bar: nil}, right: foo{Bar: &bar{}}, + exp: audit.Map{ + "bar": audit.Map{}, + }, + }, + { + name: "RightNil", + left: foo{Bar: &bar{Baz: "baz"}}, right: foo{Bar: nil}, + exp: audit.Map{ + "bar": audit.Map{ + "baz": audit.OldNew{Old: "baz", New: ""}, + }, + }, + }, + }) + }) +} + +type diffTest struct { + name string + left, right any + exp any +} + +func runDiffValuesTests(t *testing.T, table Table, tests []diffTest) { + t.Helper() + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + assert.Equal(t, + test.exp, + diffValues(test.left, test.right, table), + ) + }) + } +} + +func Test_diff(t *testing.T) { + t.Parallel() + + runDiffTests(t, []diffTest{ { name: "Create", left: audit.Empty[database.GitSSHKey](), @@ -35,7 +196,7 @@ func TestDiff(t *testing.T) { }, }) - runDiffTests(t, []diffTest[database.OrganizationMember]{ + runDiffTests(t, []diffTest{ { name: "Create", left: audit.Empty[database.OrganizationMember](), @@ -54,7 +215,7 @@ func TestDiff(t *testing.T) { }, }) - runDiffTests(t, []diffTest[database.Organization]{ + runDiffTests(t, []diffTest{ { name: "Create", left: audit.Empty[database.Organization](), @@ -73,7 +234,7 @@ func TestDiff(t *testing.T) { }, }) - runDiffTests(t, []diffTest[database.Template]{ + runDiffTests(t, []diffTest{ { name: "Create", left: audit.Empty[database.Template](), @@ -103,7 +264,7 @@ func TestDiff(t *testing.T) { }, }) - runDiffTests(t, []diffTest[database.TemplateVersion]{ + runDiffTests(t, []diffTest{ { name: "Create", left: audit.Empty[database.TemplateVersion](), @@ -145,7 +306,7 @@ func TestDiff(t *testing.T) { }, }) - runDiffTests(t, []diffTest[database.User]{ + runDiffTests(t, []diffTest{ { name: "Create", left: audit.Empty[database.User](), @@ -170,7 +331,7 @@ func TestDiff(t *testing.T) { }, }) - runDiffTests(t, []diffTest[database.Workspace]{ + runDiffTests(t, []diffTest{ { name: "Create", left: audit.Empty[database.Workspace](), @@ -216,24 +377,17 @@ func TestDiff(t *testing.T) { }) } -type diffTest[T audit.Auditable] struct { - name string - left, right T - exp audit.Map -} - -func runDiffTests[T audit.Auditable](t *testing.T, tests []diffTest[T]) { +func runDiffTests(t *testing.T, tests []diffTest) { t.Helper() - var typ T - typName := reflect.TypeOf(typ).Name() - for _, test := range tests { + typName := reflect.TypeOf(test.left).Name() + t.Run(typName+"/"+test.name, func(t *testing.T) { t.Parallel() require.Equal(t, test.exp, - audit.Diff(test.left, test.right), + (&auditor{}).diff(test.left, test.right), ) }) } diff --git a/enterprise/audit/filter.go b/enterprise/audit/filter.go new file mode 100644 index 0000000000000..868d5bb7d77db --- /dev/null +++ b/enterprise/audit/filter.go @@ -0,0 +1,42 @@ +package audit + +import ( + "context" + + "github.com/coder/coder/coderd/database" +) + +// FilterDecision is a bitwise flag describing the actions a given filter allows +// for a given audit log. +type FilterDecision uint8 + +const ( + // FilterDecisionDrop indicates that the audit log should be dropped. It + // should not be stored or exported anywhere. + FilterDecisionDrop FilterDecision = 0 + // FilterDecisionStore indicates that the audit log should be allowed to be + // stored in the Coder database. + FilterDecisionStore FilterDecision = 1 << iota + // FilterDecisionExport indicates that the audit log should be exported + // externally of Coder. + FilterDecisionExport +) + +// Filters produce a FilterDecision for a given audit log. +type Filter interface { + Check(ctx context.Context, alog database.AuditLog) (FilterDecision, error) +} + +// DefaultFilter is the default filter used when exporting audit logs. It allows +// storage and exporting for all audit logs. +var DefaultFilter Filter = FilterFunc(func(ctx context.Context, alog database.AuditLog) (FilterDecision, error) { + // Store and export all audit logs for now. + return FilterDecisionStore | FilterDecisionExport, nil +}) + +// FilterFunc constructs a Filter from a simple function. +type FilterFunc func(ctx context.Context, alog database.AuditLog) (FilterDecision, error) + +func (f FilterFunc) Check(ctx context.Context, alog database.AuditLog) (FilterDecision, error) { + return f(ctx, alog) +} diff --git a/coderd/audit/table.go b/enterprise/audit/table.go similarity index 91% rename from coderd/audit/table.go rename to enterprise/audit/table.go index 865d28831c073..aa63096366860 100644 --- a/coderd/audit/table.go +++ b/enterprise/audit/table.go @@ -6,19 +6,6 @@ import ( "github.com/coder/coder/coderd/database" ) -// 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.GitSSHKey | - database.OrganizationMember | - database.Organization | - database.Template | - database.TemplateVersion | - database.User | - database.Workspace -} - type Action string const ( diff --git a/enterprise/coderd/licenses.go b/enterprise/coderd/licenses.go index 16592fcde2654..259114519863d 100644 --- a/enterprise/coderd/licenses.go +++ b/enterprise/coderd/licenses.go @@ -18,7 +18,6 @@ import ( "golang.org/x/xerrors" "cdr.dev/slog" - "github.com/coder/coder/coderd" "github.com/coder/coder/coderd/database" "github.com/coder/coder/coderd/httpapi" diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index a399774d1011b..dabaea24a656d 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -34,6 +34,33 @@ export interface AssignableRoles extends Role { readonly assignable: boolean } +// From codersdk/audit.go +export type AuditDiff = Record + +// From codersdk/audit.go +export interface AuditLog { + readonly id: string + readonly request_id: string + readonly time: string + readonly organization_id: string + // Named type "net/netip.Addr" unknown, using "any" + // eslint-disable-next-line @typescript-eslint/no-explicit-any + readonly ip: any + readonly user_agent: string + readonly resource_type: ResourceType + readonly resource_id: string + readonly resource_target: string + readonly action: AuditAction + readonly diff: AuditDiff + readonly status_code: number + // This is likely an enum in an external package ("encoding/json.RawMessage") + readonly additional_fields: string + readonly description: string + readonly user?: User + // This is likely an enum in an external package ("encoding/json.RawMessage") + readonly resource: string +} + // From codersdk/users.go export interface AuthMethods { readonly password: boolean @@ -570,6 +597,9 @@ export interface WorkspaceResourceMetadata { readonly sensitive: boolean } +// From codersdk/audit.go +export type AuditAction = "create" | "delete" | "write" + // From codersdk/workspacebuilds.go export type BuildReason = "autostart" | "autostop" | "initiator" @@ -612,6 +642,9 @@ export type ProvisionerStorageMethod = "file" // From codersdk/organizations.go export type ProvisionerType = "echo" | "terraform" +// From codersdk/audit.go +export type ResourceType = "organization" | "template" | "template_version" | "user" | "workspace" + // From codersdk/users.go export type UserStatus = "active" | "suspended" From 823a58d4936678b856240366537f6d057b3e2309 Mon Sep 17 00:00:00 2001 From: Colin Adler Date: Mon, 29 Aug 2022 18:27:49 -0500 Subject: [PATCH 02/11] add nop auditor --- coderd/audit/audit.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/coderd/audit/audit.go b/coderd/audit/audit.go index 19086a86f408d..f1801987bbebe 100644 --- a/coderd/audit/audit.go +++ b/coderd/audit/audit.go @@ -10,3 +10,15 @@ 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{} } From b3776e0dfdf6fc03f7d69d60922193023e218fd2 Mon Sep 17 00:00:00 2001 From: Colin Adler Date: Tue, 30 Aug 2022 13:55:02 -0500 Subject: [PATCH 03/11] fixup! add nop auditor --- coderd/audit/request.go | 6 +++--- site/src/api/typesGenerated.ts | 2 ++ 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/coderd/audit/request.go b/coderd/audit/request.go index 6f15db05ab4ef..aa3520b847358 100644 --- a/coderd/audit/request.go +++ b/coderd/audit/request.go @@ -11,7 +11,7 @@ import ( "github.com/coder/coder/coderd/database" ) -type RequstParams struct { +type RequestParams struct { Audit Auditor Log slog.Logger @@ -21,7 +21,7 @@ type RequstParams struct { } type Request[T Auditable] struct { - params *RequstParams + params *RequestParams Old T New T @@ -30,7 +30,7 @@ type Request[T Auditable] struct { // InitRequest initializes an audit log for a request. It returns a function // that should be deferred, causing the audit log to be committed when the // handler returns. -func InitRequest[T Auditable](w http.ResponseWriter, p *RequstParams) (*Request[T], func()) { +func InitRequest[T Auditable](w http.ResponseWriter, p *RequestParams) (*Request[T], func()) { sw, ok := w.(chimw.WrapResponseWriter) if !ok { panic("dev error: http.ResponseWriter is not chimw.WrapResponseWriter") diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index dabaea24a656d..90235cf4f59b7 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -35,6 +35,8 @@ export interface AssignableRoles extends Role { } // From codersdk/audit.go +// Embedded anonymous struct, please fix by naming it +// eslint-disable-next-line @typescript-eslint/no-explicit-any export type AuditDiff = Record // From codersdk/audit.go From b417218dcda67ec693d7b5cd302aed49eaae0fac Mon Sep 17 00:00:00 2001 From: Colin Adler Date: Tue, 30 Aug 2022 13:57:50 -0500 Subject: [PATCH 04/11] typo --- enterprise/audit/audit.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/enterprise/audit/audit.go b/enterprise/audit/audit.go index cb1ffa4ab751a..883de84ecc4fe 100644 --- a/enterprise/audit/audit.go +++ b/enterprise/audit/audit.go @@ -15,7 +15,7 @@ type Backend interface { Export(ctx context.Context, alog database.AuditLog) error } -// auditor is the enterprise impelentation of the Auditor interface. +// auditor is the enterprise implementation of the Auditor interface. type auditor struct { //nolint:unused filter Filter From 1188f696ce0e7d4cffca07115507a47672142807 Mon Sep 17 00:00:00 2001 From: Colin Adler Date: Tue, 30 Aug 2022 16:29:10 -0500 Subject: [PATCH 05/11] fix tests --- coderd/audit/diff.go | 12 +- codersdk/audit.go | 5 +- enterprise/audit/audit.go | 4 +- enterprise/audit/diff.go | 38 +++-- enterprise/audit/diff_internal_test.go | 194 ++++++++++++------------- 5 files changed, 131 insertions(+), 122 deletions(-) diff --git a/coderd/audit/diff.go b/coderd/audit/diff.go index a9607115efc60..737cd158acb64 100644 --- a/coderd/audit/diff.go +++ b/coderd/audit/diff.go @@ -18,15 +18,15 @@ type Auditable interface { database.GitSSHKey } -// Map is a map of changed fields in an audited resource. `any` can be a -// map[string]any in the case of nested structs, or an OldNew struct -// representing a changed value. -type Map map[string]any +// 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 + Old any + New any + Secret bool } // Empty returns a default value of type T. diff --git a/codersdk/audit.go b/codersdk/audit.go index 19d3ec6f81af3..4cfa9d6002f20 100644 --- a/codersdk/audit.go +++ b/codersdk/audit.go @@ -27,8 +27,9 @@ const ( ) type AuditDiff map[string]struct { - Old any - New any + Old any + New any + Secret bool } type AuditLog struct { diff --git a/enterprise/audit/audit.go b/enterprise/audit/audit.go index 883de84ecc4fe..ea25c8ecdbf8e 100644 --- a/enterprise/audit/audit.go +++ b/enterprise/audit/audit.go @@ -29,6 +29,6 @@ func (*auditor) Export(context.Context, database.AuditLog) error { } //nolint:unused -func (*auditor) diff(any, any) audit.Map { - panic("not implemented") // TODO: Implement +func (*auditor) diff(left any, right any) audit.Map { + return diffValues(left, right, AuditableResources) } diff --git a/enterprise/audit/diff.go b/enterprise/audit/diff.go index 66d3e1cee46b9..66f1da696307e 100644 --- a/enterprise/audit/diff.go +++ b/enterprise/audit/diff.go @@ -62,20 +62,15 @@ func diffValues[T any](left, right T, table Table) audit.Map { 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] = audit.OldNew{Old: leftI, New: rightI} case ActionSecret: baseDiff[diffName] = audit.OldNew{ - Old: reflect.Zero(rightF.Type()).Interface(), - New: reflect.Zero(rightF.Type()).Interface(), + Old: reflect.Zero(rightF.Type()).Interface(), + New: reflect.Zero(rightF.Type()).Interface(), + Secret: true, } } } @@ -88,18 +83,31 @@ func diffValues[T any](left, right T, table Table) audit.Map { // //nolint:forcetypeassert func convertDiffType(left, right any) (newLeft, newRight any, changed bool) { - switch typed := left.(type) { + switch typedLeft := left.(type) { case uuid.UUID: - return typed.String(), right.(uuid.UUID).String(), true + typedRight := right.(uuid.UUID) + + // Automatically coerce Nil UUIDs to empty strings. + outLeft := typedLeft.String() + if typedLeft == uuid.Nil { + outLeft = "" + } + + outRight := typedRight.String() + if typedRight == uuid.Nil { + outRight = "" + } + + return outLeft, outRight, true case uuid.NullUUID: - leftStr, _ := typed.MarshalText() + leftStr, _ := typedLeft.MarshalText() rightStr, _ := right.(uuid.NullUUID).MarshalText() return string(leftStr), string(rightStr), true case sql.NullString: - leftStr := typed.String - if !typed.Valid { + leftStr := typedLeft.String + if !typedLeft.Valid { leftStr = "null" } @@ -113,10 +121,10 @@ func convertDiffType(left, right any) (newLeft, newRight any, changed bool) { case sql.NullInt64: var leftInt64Ptr *int64 var rightInt64Ptr *int64 - if !typed.Valid { + if !typedLeft.Valid { leftInt64Ptr = nil } else { - leftInt64Ptr = ptr(typed.Int64) + leftInt64Ptr = ptr(typedLeft.Int64) } rightInt64Ptr = ptr(right.(sql.NullInt64).Int64) diff --git a/enterprise/audit/diff_internal_test.go b/enterprise/audit/diff_internal_test.go index 0c7526a8050f3..d63c1b0ac0703 100644 --- a/enterprise/audit/diff_internal_test.go +++ b/enterprise/audit/diff_internal_test.go @@ -23,7 +23,7 @@ func Test_diffValues(t *testing.T) { type foo struct { Bar string `json:"bar"` - Baz int64 `json:"baz"` + Baz int `json:"baz"` } table := auditMap(map[any]map[string]Action{ @@ -95,64 +95,64 @@ func Test_diffValues(t *testing.T) { }, }) }) + // We currently don't support nested structs. + // t.Run("NestedStruct", func(t *testing.T) { + // t.Parallel() - t.Run("NestedStruct", func(t *testing.T) { - t.Parallel() - - type bar struct { - Baz string `json:"baz"` - } + // type bar struct { + // Baz string `json:"baz"` + // } - type foo struct { - Bar *bar `json:"bar"` - } + // type foo struct { + // Bar *bar `json:"bar"` + // } - table := auditMap(map[any]map[string]Action{ - &foo{}: { - "bar": ActionTrack, - }, - &bar{}: { - "baz": ActionTrack, - }, - }) + // table := auditMap(map[any]map[string]Action{ + // &foo{}: { + // "bar": ActionTrack, + // }, + // &bar{}: { + // "baz": ActionTrack, + // }, + // }) - runDiffValuesTests(t, table, []diffTest{ - { - name: "LeftEmpty", - left: foo{Bar: &bar{}}, right: foo{Bar: &bar{Baz: "baz"}}, - exp: audit.Map{ - "bar": audit.Map{ - "baz": audit.OldNew{Old: "", New: "baz"}, - }, - }, - }, - { - name: "RightEmpty", - left: foo{Bar: &bar{Baz: "baz"}}, right: foo{Bar: &bar{}}, - exp: audit.Map{ - "bar": audit.Map{ - "baz": audit.OldNew{Old: "baz", New: ""}, - }, - }, - }, - { - name: "LeftNil", - left: foo{Bar: nil}, right: foo{Bar: &bar{}}, - exp: audit.Map{ - "bar": audit.Map{}, - }, - }, - { - name: "RightNil", - left: foo{Bar: &bar{Baz: "baz"}}, right: foo{Bar: nil}, - exp: audit.Map{ - "bar": audit.Map{ - "baz": audit.OldNew{Old: "baz", New: ""}, - }, - }, - }, - }) - }) + // runDiffValuesTests(t, table, []diffTest{ + // { + // name: "LeftEmpty", + // left: foo{Bar: &bar{}}, right: foo{Bar: &bar{Baz: "baz"}}, + // exp: audit.Map{ + // "bar": audit.Map{ + // "baz": audit.OldNew{Old: "", New: "baz"}, + // }, + // }, + // }, + // { + // name: "RightEmpty", + // left: foo{Bar: &bar{Baz: "baz"}}, right: foo{Bar: &bar{}}, + // exp: audit.Map{ + // "bar": audit.Map{ + // "baz": audit.OldNew{Old: "baz", New: ""}, + // }, + // }, + // }, + // { + // name: "LeftNil", + // left: foo{Bar: nil}, right: foo{Bar: &bar{}}, + // exp: audit.Map{ + // "bar": audit.Map{}, + // }, + // }, + // { + // name: "RightNil", + // left: foo{Bar: &bar{Baz: "baz"}}, right: foo{Bar: nil}, + // exp: audit.Map{ + // "bar": audit.Map{ + // "baz": audit.OldNew{Old: "baz", New: ""}, + // }, + // }, + // }, + // }) + // }) } type diffTest struct { @@ -189,9 +189,9 @@ func Test_diff(t *testing.T) { PublicKey: "a very public public key", }, exp: audit.Map{ - "user_id": uuid.UUID{1}.String(), - "private_key": "", - "public_key": "a very public public key", + "user_id": audit.OldNew{Old: "", New: uuid.UUID{1}.String()}, + "private_key": audit.OldNew{Old: "", New: "", Secret: true}, + "public_key": audit.OldNew{Old: "", New: "a very public public key"}, }, }, }) @@ -208,9 +208,9 @@ func Test_diff(t *testing.T) { Roles: []string{"auditor"}, }, exp: audit.Map{ - "user_id": uuid.UUID{1}.String(), - "organization_id": uuid.UUID{2}.String(), - "roles": []string{"auditor"}, + "user_id": audit.OldNew{Old: "", New: uuid.UUID{1}.String()}, + "organization_id": audit.OldNew{Old: "", New: uuid.UUID{2}.String()}, + "roles": audit.OldNew{Old: ([]string)(nil), New: []string{"auditor"}}, }, }, }) @@ -227,9 +227,9 @@ func Test_diff(t *testing.T) { UpdatedAt: time.Now(), }, exp: audit.Map{ - "id": uuid.UUID{1}.String(), - "name": "rust developers", - "description": "an organization for rust developers", + "id": audit.OldNew{Old: "", New: uuid.UUID{1}.String()}, + "name": audit.OldNew{Old: "", New: "rust developers"}, + "description": audit.OldNew{Old: "", New: "an organization for rust developers"}, }, }, }) @@ -252,14 +252,14 @@ func Test_diff(t *testing.T) { CreatedBy: uuid.UUID{4}, }, exp: audit.Map{ - "id": uuid.UUID{1}.String(), - "organization_id": uuid.UUID{2}.String(), - "name": "rust", - "provisioner": database.ProvisionerTypeTerraform, - "active_version_id": uuid.UUID{3}.String(), - "max_ttl": int64(3600000000000), - "min_autostart_interval": int64(60000000000), - "created_by": uuid.UUID{4}.String(), + "id": audit.OldNew{Old: "", New: uuid.UUID{1}.String()}, + "organization_id": audit.OldNew{Old: "", New: uuid.UUID{2}.String()}, + "name": audit.OldNew{Old: "", New: "rust"}, + "provisioner": audit.OldNew{Old: database.ProvisionerType(""), New: database.ProvisionerTypeTerraform}, + "active_version_id": audit.OldNew{Old: "", New: uuid.UUID{3}.String()}, + "max_ttl": audit.OldNew{Old: int64(0), New: int64(time.Hour)}, + "min_autostart_interval": audit.OldNew{Old: int64(0), New: int64(time.Minute)}, + "created_by": audit.OldNew{Old: "", New: uuid.UUID{4}.String()}, }, }, }) @@ -278,11 +278,11 @@ func Test_diff(t *testing.T) { CreatedBy: uuid.NullUUID{UUID: uuid.UUID{4}, Valid: true}, }, exp: audit.Map{ - "id": uuid.UUID{1}.String(), - "template_id": uuid.UUID{2}.String(), - "organization_id": uuid.UUID{3}.String(), - "name": "rust", - "created_by": uuid.UUID{4}.String(), + "id": audit.OldNew{Old: "", New: uuid.UUID{1}.String()}, + "template_id": audit.OldNew{Old: "", New: uuid.UUID{2}.String()}, + "organization_id": audit.OldNew{Old: "", New: uuid.UUID{3}.String()}, + "created_by": audit.OldNew{Old: "", New: uuid.UUID{4}.String()}, + "name": audit.OldNew{Old: "", New: "rust"}, }, }, { @@ -298,10 +298,10 @@ func Test_diff(t *testing.T) { CreatedBy: uuid.NullUUID{UUID: uuid.UUID{4}, Valid: true}, }, exp: audit.Map{ - "id": uuid.UUID{1}.String(), - "organization_id": uuid.UUID{3}.String(), - "name": "rust", - "created_by": uuid.UUID{4}.String(), + "id": audit.OldNew{Old: "", New: uuid.UUID{1}.String()}, + "organization_id": audit.OldNew{Old: "", New: uuid.UUID{3}.String()}, + "created_by": audit.OldNew{Old: "null", New: uuid.UUID{4}.String()}, + "name": audit.OldNew{Old: "", New: "rust"}, }, }, }) @@ -321,12 +321,12 @@ func Test_diff(t *testing.T) { RBACRoles: []string{"omega admin"}, }, exp: audit.Map{ - "id": uuid.UUID{1}.String(), - "email": "colin@coder.com", - "username": "colin", - "hashed_password": ([]byte)(nil), - "status": database.UserStatusActive, - "rbac_roles": []string{"omega admin"}, + "id": audit.OldNew{Old: "", New: uuid.UUID{1}.String()}, + "email": audit.OldNew{Old: "", New: "colin@coder.com"}, + "username": audit.OldNew{Old: "", New: "colin"}, + "hashed_password": audit.OldNew{Old: ([]byte)(nil), New: ([]byte)(nil), Secret: true}, + "status": audit.OldNew{Old: database.UserStatus(""), New: database.UserStatusActive}, + "rbac_roles": audit.OldNew{Old: ([]string)(nil), New: []string{"omega admin"}}, }, }, }) @@ -346,12 +346,12 @@ func Test_diff(t *testing.T) { Ttl: sql.NullInt64{Int64: int64(8 * time.Hour), Valid: true}, }, exp: audit.Map{ - "id": uuid.UUID{1}.String(), - "owner_id": uuid.UUID{2}.String(), - "template_id": uuid.UUID{3}.String(), - "name": "rust workspace", - "autostart_schedule": "0 12 * * 1-5", - "ttl": int64(28800000000000), // XXX: pq still does not support time.Duration + "id": audit.OldNew{Old: "", New: uuid.UUID{1}.String()}, + "owner_id": audit.OldNew{Old: "", New: uuid.UUID{2}.String()}, + "template_id": audit.OldNew{Old: "", New: uuid.UUID{3}.String()}, + "name": audit.OldNew{Old: "", New: "rust workspace"}, + "autostart_schedule": audit.OldNew{Old: "", New: "0 12 * * 1-5"}, + "ttl": audit.OldNew{Old: int64(0), New: int64(8 * time.Hour)}, // XXX: pq still does not support time.Duration }, }, { @@ -368,10 +368,10 @@ func Test_diff(t *testing.T) { Ttl: sql.NullInt64{}, }, exp: audit.Map{ - "id": uuid.UUID{1}.String(), - "owner_id": uuid.UUID{2}.String(), - "template_id": uuid.UUID{3}.String(), - "name": "rust workspace", + "id": audit.OldNew{Old: "", New: uuid.UUID{1}.String()}, + "owner_id": audit.OldNew{Old: "", New: uuid.UUID{2}.String()}, + "template_id": audit.OldNew{Old: "", New: uuid.UUID{3}.String()}, + "name": audit.OldNew{Old: "", New: "rust workspace"}, }, }, }) From 595f4c1c82f67cc8b09e53a0d5eea35ff65693ef Mon Sep 17 00:00:00 2001 From: Colin Adler Date: Tue, 30 Aug 2022 16:34:01 -0500 Subject: [PATCH 06/11] fixup! fix tests --- enterprise/audit/diff_internal_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/enterprise/audit/diff_internal_test.go b/enterprise/audit/diff_internal_test.go index d63c1b0ac0703..0f4c13ae18873 100644 --- a/enterprise/audit/diff_internal_test.go +++ b/enterprise/audit/diff_internal_test.go @@ -65,6 +65,7 @@ func Test_diffValues(t *testing.T) { }) }) + //nolint:revive t.Run("PointerField", func(t *testing.T) { t.Parallel() @@ -95,6 +96,7 @@ func Test_diffValues(t *testing.T) { }, }) }) + // We currently don't support nested structs. // t.Run("NestedStruct", func(t *testing.T) { // t.Parallel() From 8f915f6f935242e0ea59f8717593d1e2ca7ab7e4 Mon Sep 17 00:00:00 2001 From: Colin Adler Date: Tue, 30 Aug 2022 22:04:20 -0500 Subject: [PATCH 07/11] add auditor constructor --- coderd/audit/diff.go | 8 ++++++++ enterprise/audit/audit.go | 15 ++++++++++----- enterprise/audit/diff.go | 2 +- enterprise/audit/diff_internal_test.go | 2 +- 4 files changed, 20 insertions(+), 7 deletions(-) diff --git a/coderd/audit/diff.go b/coderd/audit/diff.go index 737cd158acb64..537abc1b8bb27 100644 --- a/coderd/audit/diff.go +++ b/coderd/audit/diff.go @@ -38,3 +38,11 @@ func Empty[T Auditable]() T { // Diff compares two auditable resources and produces a Map of the changed // values. func Diff[T Auditable](a Auditor, left, right T) Map { return a.diff(left, right) } + +type Differ struct { + DiffFn func(old, new any) Map +} + +func (d Differ) diff(old, new any) Map { + return d.DiffFn(old, new) +} diff --git a/enterprise/audit/audit.go b/enterprise/audit/audit.go index ea25c8ecdbf8e..f4a27962103cd 100644 --- a/enterprise/audit/audit.go +++ b/enterprise/audit/audit.go @@ -15,20 +15,25 @@ type Backend interface { Export(ctx context.Context, alog database.AuditLog) error } +func NewAuditor() audit.Auditor { + return &auditor{ + Differ: audit.Differ{DiffFn: func(old, new any) audit.Map { + return diffValues(old, new, AuditableResources) + }}, + } +} + // auditor is the enterprise implementation of the Auditor interface. type auditor struct { //nolint:unused filter Filter //nolint:unused backends []Backend + + audit.Differ } //nolint:unused func (*auditor) Export(context.Context, database.AuditLog) error { panic("not implemented") // TODO: Implement } - -//nolint:unused -func (*auditor) diff(left any, right any) audit.Map { - return diffValues(left, right, AuditableResources) -} diff --git a/enterprise/audit/diff.go b/enterprise/audit/diff.go index 66f1da696307e..7602826bd30cc 100644 --- a/enterprise/audit/diff.go +++ b/enterprise/audit/diff.go @@ -14,7 +14,7 @@ func structName(t reflect.Type) string { return t.PkgPath() + "." + t.Name() } -func diffValues[T any](left, right T, table Table) audit.Map { +func diffValues(left, right any, table Table) audit.Map { var ( baseDiff = audit.Map{} diff --git a/enterprise/audit/diff_internal_test.go b/enterprise/audit/diff_internal_test.go index 0f4c13ae18873..bdc4e87b7c30f 100644 --- a/enterprise/audit/diff_internal_test.go +++ b/enterprise/audit/diff_internal_test.go @@ -389,7 +389,7 @@ func runDiffTests(t *testing.T, tests []diffTest) { t.Parallel() require.Equal(t, test.exp, - (&auditor{}).diff(test.left, test.right), + diffValues(test.left, test.right, AuditableResources), ) }) } From 10a0d3b1d892d69e25643eb02cb89a75abb441a0 Mon Sep 17 00:00:00 2001 From: Colin Adler Date: Wed, 31 Aug 2022 13:58:12 -0500 Subject: [PATCH 08/11] spike comments --- coderd/audit.go | 1 - codersdk/audit.go | 24 ++++++++++++++---------- site/src/api/typesGenerated.ts | 13 ++++++++++--- 3 files changed, 24 insertions(+), 14 deletions(-) delete mode 100644 coderd/audit.go diff --git a/coderd/audit.go b/coderd/audit.go deleted file mode 100644 index ddb2b8b672186..0000000000000 --- a/coderd/audit.go +++ /dev/null @@ -1 +0,0 @@ -package coderd diff --git a/codersdk/audit.go b/codersdk/audit.go index 4cfa9d6002f20..043fc210aa061 100644 --- a/codersdk/audit.go +++ b/codersdk/audit.go @@ -26,21 +26,24 @@ const ( AuditActionDelete AuditAction = "delete" ) -type AuditDiff map[string]struct { +type AuditDiff map[string]AuditDiffField + +type AuditDiffField struct { Old any New any Secret bool } type AuditLog struct { - ID uuid.UUID `json:"id"` - RequestID uuid.UUID `json:"request_id"` - Time time.Time `json:"time"` - OrganizationID uuid.UUID `json:"organization_id"` - IP netip.Addr `json:"ip"` - UserAgent string `json:"user_agent"` - ResourceType ResourceType `json:"resource_type"` - ResourceID uuid.UUID `json:"resource_id"` + ID uuid.UUID `json:"id"` + RequestID uuid.UUID `json:"request_id"` + Time time.Time `json:"time"` + OrganizationID uuid.UUID `json:"organization_id"` + IP netip.Addr `json:"ip"` + UserAgent string `json:"user_agent"` + ResourceType ResourceType `json:"resource_type"` + ResourceID uuid.UUID `json:"resource_id"` + // ResourceTarget is the name of the resource. ResourceTarget string `json:"resource_target"` Action AuditAction `json:"action"` Diff AuditDiff `json:"diff"` @@ -48,6 +51,7 @@ type AuditLog struct { AdditionalFields json.RawMessage `json:"additional_fields"` Description string `json:"description"` - User *User `json:"user"` + User *User `json:"user"` + // Resource is the full object of the resource the audit log is for. Resource json.RawMessage `json:"resource"` } diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 90235cf4f59b7..d6969baa41fd9 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -35,9 +35,16 @@ export interface AssignableRoles extends Role { } // From codersdk/audit.go -// Embedded anonymous struct, please fix by naming it -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export type AuditDiff = Record +export type AuditDiff = Record + +// From codersdk/audit.go +export interface AuditDiffField { + // eslint-disable-next-line + readonly Old: any + // eslint-disable-next-line + readonly New: any + readonly Secret: boolean +} // From codersdk/audit.go export interface AuditLog { From ec83dfcc1d4168768aae3452c380e8577ed735ed Mon Sep 17 00:00:00 2001 From: Colin Adler Date: Wed, 31 Aug 2022 16:00:38 -0500 Subject: [PATCH 09/11] fixup! spike comments --- coderd/database/dump.sql | 3 ++- .../migrations/000039_audit_addtl_fields.down.sql | 3 ++- .../migrations/000039_audit_addtl_fields.up.sql | 13 +++++++++---- coderd/database/models.go | 1 + coderd/database/queries.sql.go | 11 ++++++++--- coderd/database/queries/auditlogs.sql | 5 +++-- codersdk/audit.go | 3 +-- site/src/api/typesGenerated.ts | 3 +-- 8 files changed, 27 insertions(+), 15 deletions(-) diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index 13de9addc0e9a..5791b53d1e652 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -114,7 +114,8 @@ CREATE TABLE audit_logs ( diff jsonb NOT NULL, status_code integer NOT NULL, additional_fields jsonb NOT NULL, - request_id uuid NOT NULL + request_id uuid NOT NULL, + resource_icon text NOT NULL ); CREATE TABLE files ( diff --git a/coderd/database/migrations/000039_audit_addtl_fields.down.sql b/coderd/database/migrations/000039_audit_addtl_fields.down.sql index 843aa19f6f4b2..81a89726906cd 100644 --- a/coderd/database/migrations/000039_audit_addtl_fields.down.sql +++ b/coderd/database/migrations/000039_audit_addtl_fields.down.sql @@ -1,3 +1,4 @@ ALTER TABLE audit_logs DROP COLUMN additional_fields, - DROP COLUMN request_id; + DROP COLUMN request_id, + DROP COLUMN resource_icon; diff --git a/coderd/database/migrations/000039_audit_addtl_fields.up.sql b/coderd/database/migrations/000039_audit_addtl_fields.up.sql index dc51bb9075dd9..891cbe3b99553 100644 --- a/coderd/database/migrations/000039_audit_addtl_fields.up.sql +++ b/coderd/database/migrations/000039_audit_addtl_fields.up.sql @@ -1,4 +1,9 @@ -ALTER TABLE audit_logs ADD COLUMN additional_fields jsonb NOT NULL DEFAULT '{}'::jsonb; -ALTER TABLE audit_logs ALTER COLUMN additional_fields DROP DEFAULT; -ALTER TABLE audit_logs ADD COLUMN request_id uuid NOT NULL DEFAULT '00000000-0000-0000-0000-000000000000'::uuid; -ALTER TABLE audit_logs ALTER COLUMN request_id DROP DEFAULT; +ALTER TABLE audit_logs + ADD COLUMN additional_fields jsonb NOT NULL DEFAULT '{}'::jsonb, + ADD COLUMN request_id uuid NOT NULL DEFAULT '00000000-0000-0000-0000-000000000000'::uuid, + ADD COLUMN resource_icon text NOT NULL DEFAULT ''; + +ALTER TABLE audit_logs + ALTER COLUMN additional_fields DROP DEFAULT, + ALTER COLUMN request_id DROP DEFAULT, + ALTER COLUMN resource_icon DROP DEFAULT; diff --git a/coderd/database/models.go b/coderd/database/models.go index ad7f7a151bf60..58006643730db 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -340,6 +340,7 @@ type AuditLog struct { StatusCode int32 `db:"status_code" json:"status_code"` AdditionalFields json.RawMessage `db:"additional_fields" json:"additional_fields"` RequestID uuid.UUID `db:"request_id" json:"request_id"` + ResourceIcon string `db:"resource_icon" json:"resource_icon"` } type File struct { diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index b8b2a65ef8cca..7f99041753f84 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -192,7 +192,7 @@ func (q *sqlQuerier) UpdateAPIKeyByID(ctx context.Context, arg UpdateAPIKeyByIDP const getAuditLogsBefore = `-- name: GetAuditLogsBefore :many SELECT - id, time, user_id, organization_id, ip, user_agent, resource_type, resource_id, resource_target, action, diff, status_code, additional_fields, request_id + id, time, user_id, organization_id, ip, user_agent, resource_type, resource_id, resource_target, action, diff, status_code, additional_fields, request_id, resource_icon FROM audit_logs WHERE @@ -235,6 +235,7 @@ func (q *sqlQuerier) GetAuditLogsBefore(ctx context.Context, arg GetAuditLogsBef &i.StatusCode, &i.AdditionalFields, &i.RequestID, + &i.ResourceIcon, ); err != nil { return nil, err } @@ -265,10 +266,11 @@ INSERT INTO diff, status_code, additional_fields, - request_id + request_id, + resource_icon ) VALUES - ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14) RETURNING id, time, user_id, organization_id, ip, user_agent, resource_type, resource_id, resource_target, action, diff, status_code, additional_fields, request_id + ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15) RETURNING id, time, user_id, organization_id, ip, user_agent, resource_type, resource_id, resource_target, action, diff, status_code, additional_fields, request_id, resource_icon ` type InsertAuditLogParams struct { @@ -286,6 +288,7 @@ type InsertAuditLogParams struct { StatusCode int32 `db:"status_code" json:"status_code"` AdditionalFields json.RawMessage `db:"additional_fields" json:"additional_fields"` RequestID uuid.UUID `db:"request_id" json:"request_id"` + ResourceIcon string `db:"resource_icon" json:"resource_icon"` } func (q *sqlQuerier) InsertAuditLog(ctx context.Context, arg InsertAuditLogParams) (AuditLog, error) { @@ -304,6 +307,7 @@ func (q *sqlQuerier) InsertAuditLog(ctx context.Context, arg InsertAuditLogParam arg.StatusCode, arg.AdditionalFields, arg.RequestID, + arg.ResourceIcon, ) var i AuditLog err := row.Scan( @@ -321,6 +325,7 @@ func (q *sqlQuerier) InsertAuditLog(ctx context.Context, arg InsertAuditLogParam &i.StatusCode, &i.AdditionalFields, &i.RequestID, + &i.ResourceIcon, ) return i, err } diff --git a/coderd/database/queries/auditlogs.sql b/coderd/database/queries/auditlogs.sql index d9f80a0967b18..cb87ce065e4e2 100644 --- a/coderd/database/queries/auditlogs.sql +++ b/coderd/database/queries/auditlogs.sql @@ -28,7 +28,8 @@ INSERT INTO diff, status_code, additional_fields, - request_id + request_id, + resource_icon ) VALUES - ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14) RETURNING *; + ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15) RETURNING *; diff --git a/codersdk/audit.go b/codersdk/audit.go index 043fc210aa061..c9a7296cb104d 100644 --- a/codersdk/audit.go +++ b/codersdk/audit.go @@ -45,6 +45,7 @@ type AuditLog struct { ResourceID uuid.UUID `json:"resource_id"` // ResourceTarget is the name of the resource. ResourceTarget string `json:"resource_target"` + ResourceIcon string `json:"resource_icon"` Action AuditAction `json:"action"` Diff AuditDiff `json:"diff"` StatusCode int32 `json:"status_code"` @@ -52,6 +53,4 @@ type AuditLog struct { Description string `json:"description"` User *User `json:"user"` - // Resource is the full object of the resource the audit log is for. - Resource json.RawMessage `json:"resource"` } diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index d6969baa41fd9..02ffd9f707de7 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -59,6 +59,7 @@ export interface AuditLog { readonly resource_type: ResourceType readonly resource_id: string readonly resource_target: string + readonly resource_icon: string readonly action: AuditAction readonly diff: AuditDiff readonly status_code: number @@ -66,8 +67,6 @@ export interface AuditLog { readonly additional_fields: string readonly description: string readonly user?: User - // This is likely an enum in an external package ("encoding/json.RawMessage") - readonly resource: string } // From codersdk/users.go From 9cfcbebb6ed4f6ce23ed52bcff81d91f4525f110 Mon Sep 17 00:00:00 2001 From: Colin Adler Date: Wed, 31 Aug 2022 16:01:19 -0500 Subject: [PATCH 10/11] fixup! Merge branch 'main' into colin/auditv2 --- ...t_addtl_fields.down.sql => 000040_audit_addtl_fields.down.sql} | 0 ...audit_addtl_fields.up.sql => 000040_audit_addtl_fields.up.sql} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename coderd/database/migrations/{000039_audit_addtl_fields.down.sql => 000040_audit_addtl_fields.down.sql} (100%) rename coderd/database/migrations/{000039_audit_addtl_fields.up.sql => 000040_audit_addtl_fields.up.sql} (100%) diff --git a/coderd/database/migrations/000039_audit_addtl_fields.down.sql b/coderd/database/migrations/000040_audit_addtl_fields.down.sql similarity index 100% rename from coderd/database/migrations/000039_audit_addtl_fields.down.sql rename to coderd/database/migrations/000040_audit_addtl_fields.down.sql diff --git a/coderd/database/migrations/000039_audit_addtl_fields.up.sql b/coderd/database/migrations/000040_audit_addtl_fields.up.sql similarity index 100% rename from coderd/database/migrations/000039_audit_addtl_fields.up.sql rename to coderd/database/migrations/000040_audit_addtl_fields.up.sql From c3115ecb843296b5cc07ecdc3dc59c69b8ff89ae Mon Sep 17 00:00:00 2001 From: Colin Adler Date: Wed, 31 Aug 2022 16:02:48 -0500 Subject: [PATCH 11/11] comment why Differ is necessary --- coderd/audit/diff.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/coderd/audit/diff.go b/coderd/audit/diff.go index 537abc1b8bb27..8d0b5494568f8 100644 --- a/coderd/audit/diff.go +++ b/coderd/audit/diff.go @@ -39,10 +39,14 @@ func Empty[T Auditable]() T { // values. func Diff[T Auditable](a Auditor, left, right T) Map { return a.diff(left, right) } +// Differ is used so the enterprise version can implement the diff function in +// the Auditor feature interface. Only types in the same package as the +// interface can implement unexported methods. type Differ struct { DiffFn func(old, new any) Map } +//nolint:unused func (d Differ) diff(old, new any) Map { return d.DiffFn(old, new) }