diff --git a/coderd/audit/audit.go b/coderd/audit/audit.go new file mode 100644 index 0000000000000..f1801987bbebe --- /dev/null +++ b/coderd/audit/audit.go @@ -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{} } diff --git a/coderd/audit/diff.go b/coderd/audit/diff.go index efb8d87548f86..8d0b5494568f8 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. 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 @@ -18,153 +37,16 @@ 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) - } +func Diff[T Auditable](a Auditor, left, right T) Map { return a.diff(left, right) } - return ptr +// 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 } -func ptr[T any](x T) *T { - return &x +//nolint:unused +func (d Differ) diff(old, new any) Map { + return d.DiffFn(old, new) } 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/diff_test.go b/coderd/audit/diff_test.go deleted file mode 100644 index 0e90d8c30dcad..0000000000000 --- a/coderd/audit/diff_test.go +++ /dev/null @@ -1,240 +0,0 @@ -package audit_test - -import ( - "database/sql" - "reflect" - "testing" - "time" - - "github.com/google/uuid" - "github.com/stretchr/testify/require" - - "github.com/coder/coder/coderd/audit" - "github.com/coder/coder/coderd/database" -) - -func TestDiff(t *testing.T) { - t.Parallel() - - runDiffTests(t, []diffTest[database.GitSSHKey]{ - { - name: "Create", - left: audit.Empty[database.GitSSHKey](), - right: database.GitSSHKey{ - UserID: uuid.UUID{1}, - CreatedAt: time.Now(), - UpdatedAt: time.Now(), - PrivateKey: "a very secret private key", - PublicKey: "a very public public key", - }, - exp: audit.Map{ - "user_id": uuid.UUID{1}.String(), - "private_key": "", - "public_key": "a very public public key", - }, - }, - }) - - runDiffTests(t, []diffTest[database.OrganizationMember]{ - { - name: "Create", - left: audit.Empty[database.OrganizationMember](), - right: database.OrganizationMember{ - UserID: uuid.UUID{1}, - OrganizationID: uuid.UUID{2}, - CreatedAt: time.Now(), - UpdatedAt: time.Now(), - Roles: []string{"auditor"}, - }, - exp: audit.Map{ - "user_id": uuid.UUID{1}.String(), - "organization_id": uuid.UUID{2}.String(), - "roles": []string{"auditor"}, - }, - }, - }) - - runDiffTests(t, []diffTest[database.Organization]{ - { - name: "Create", - left: audit.Empty[database.Organization](), - right: database.Organization{ - ID: uuid.UUID{1}, - Name: "rust developers", - Description: "an organization for rust developers", - CreatedAt: time.Now(), - UpdatedAt: time.Now(), - }, - exp: audit.Map{ - "id": uuid.UUID{1}.String(), - "name": "rust developers", - "description": "an organization for rust developers", - }, - }, - }) - - runDiffTests(t, []diffTest[database.Template]{ - { - name: "Create", - left: audit.Empty[database.Template](), - right: database.Template{ - ID: uuid.UUID{1}, - CreatedAt: time.Now(), - UpdatedAt: time.Now(), - OrganizationID: uuid.UUID{2}, - Deleted: false, - Name: "rust", - Provisioner: database.ProvisionerTypeTerraform, - ActiveVersionID: uuid.UUID{3}, - MaxTtl: int64(time.Hour), - MinAutostartInterval: int64(time.Minute), - 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(), - }, - }, - }) - - runDiffTests(t, []diffTest[database.TemplateVersion]{ - { - name: "Create", - left: audit.Empty[database.TemplateVersion](), - right: database.TemplateVersion{ - ID: uuid.UUID{1}, - TemplateID: uuid.NullUUID{UUID: uuid.UUID{2}, Valid: true}, - CreatedAt: time.Now(), - UpdatedAt: time.Now(), - OrganizationID: uuid.UUID{3}, - Name: "rust", - 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(), - }, - }, - { - name: "CreateNullTemplateID", - left: audit.Empty[database.TemplateVersion](), - right: database.TemplateVersion{ - ID: uuid.UUID{1}, - TemplateID: uuid.NullUUID{}, - CreatedAt: time.Now(), - UpdatedAt: time.Now(), - OrganizationID: uuid.UUID{3}, - Name: "rust", - 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(), - }, - }, - }) - - runDiffTests(t, []diffTest[database.User]{ - { - name: "Create", - left: audit.Empty[database.User](), - right: database.User{ - ID: uuid.UUID{1}, - Email: "colin@coder.com", - Username: "colin", - HashedPassword: []byte("hunter2ButHashed"), - CreatedAt: time.Now(), - UpdatedAt: time.Now(), - Status: database.UserStatusActive, - 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"}, - }, - }, - }) - - runDiffTests(t, []diffTest[database.Workspace]{ - { - name: "Create", - left: audit.Empty[database.Workspace](), - right: database.Workspace{ - ID: uuid.UUID{1}, - CreatedAt: time.Now(), - UpdatedAt: time.Now(), - OwnerID: uuid.UUID{2}, - TemplateID: uuid.UUID{3}, - Name: "rust workspace", - AutostartSchedule: sql.NullString{String: "0 12 * * 1-5", Valid: true}, - 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 - }, - }, - { - name: "NullSchedules", - left: audit.Empty[database.Workspace](), - right: database.Workspace{ - ID: uuid.UUID{1}, - CreatedAt: time.Now(), - UpdatedAt: time.Now(), - OwnerID: uuid.UUID{2}, - TemplateID: uuid.UUID{3}, - Name: "rust workspace", - AutostartSchedule: sql.NullString{}, - 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", - }, - }, - }) -} - -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]) { - t.Helper() - - var typ T - typName := reflect.TypeOf(typ).Name() - - for _, test := range tests { - t.Run(typName+"/"+test.name, func(t *testing.T) { - t.Parallel() - require.Equal(t, - test.exp, - audit.Diff(test.left, test.right), - ) - }) - } -} diff --git a/coderd/audit/request.go b/coderd/audit/request.go new file mode 100644 index 0000000000000..aa3520b847358 --- /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 RequestParams struct { + Audit Auditor + Log slog.Logger + + Action database.AuditAction + ResourceType database.ResourceType + Actor uuid.UUID +} + +type Request[T Auditable] struct { + params *RequestParams + + 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 *RequestParams) (*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 c004242bd565b..25c2b45d4fa79 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -112,7 +112,10 @@ 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, + resource_icon text NOT NULL ); CREATE TABLE files ( diff --git a/coderd/database/migrations/000040_audit_addtl_fields.down.sql b/coderd/database/migrations/000040_audit_addtl_fields.down.sql new file mode 100644 index 0000000000000..81a89726906cd --- /dev/null +++ b/coderd/database/migrations/000040_audit_addtl_fields.down.sql @@ -0,0 +1,4 @@ +ALTER TABLE audit_logs + DROP COLUMN additional_fields, + DROP COLUMN request_id, + DROP COLUMN resource_icon; diff --git a/coderd/database/migrations/000040_audit_addtl_fields.up.sql b/coderd/database/migrations/000040_audit_addtl_fields.up.sql new file mode 100644 index 0000000000000..891cbe3b99553 --- /dev/null +++ b/coderd/database/migrations/000040_audit_addtl_fields.up.sql @@ -0,0 +1,9 @@ +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 4139e9b16b116..be6fe1bbfbbb5 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -326,18 +326,21 @@ 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"` + 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 a092c5def93c5..d3f15006c834b 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, resource_icon FROM audit_logs WHERE @@ -233,6 +233,9 @@ func (q *sqlQuerier) GetAuditLogsBefore(ctx context.Context, arg GetAuditLogsBef &i.Action, &i.Diff, &i.StatusCode, + &i.AdditionalFields, + &i.RequestID, + &i.ResourceIcon, ); err != nil { return nil, err } @@ -261,25 +264,31 @@ INSERT INTO resource_target, action, diff, - status_code + status_code, + additional_fields, + request_id, + resource_icon ) 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, $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 { - 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"` + ResourceIcon string `db:"resource_icon" json:"resource_icon"` } func (q *sqlQuerier) InsertAuditLog(ctx context.Context, arg InsertAuditLogParams) (AuditLog, error) { @@ -296,6 +305,9 @@ func (q *sqlQuerier) InsertAuditLog(ctx context.Context, arg InsertAuditLogParam arg.Action, arg.Diff, arg.StatusCode, + arg.AdditionalFields, + arg.RequestID, + arg.ResourceIcon, ) var i AuditLog err := row.Scan( @@ -311,6 +323,9 @@ func (q *sqlQuerier) InsertAuditLog(ctx context.Context, arg InsertAuditLogParam &i.Action, &i.Diff, &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 e827ea1cd113f..cb87ce065e4e2 100644 --- a/coderd/database/queries/auditlogs.sql +++ b/coderd/database/queries/auditlogs.sql @@ -26,7 +26,10 @@ INSERT INTO resource_target, action, diff, - status_code + status_code, + additional_fields, + request_id, + resource_icon ) 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, $15) RETURNING *; diff --git a/codersdk/audit.go b/codersdk/audit.go new file mode 100644 index 0000000000000..c9a7296cb104d --- /dev/null +++ b/codersdk/audit.go @@ -0,0 +1,56 @@ +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]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"` + // 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"` + AdditionalFields json.RawMessage `json:"additional_fields"` + Description string `json:"description"` + + User *User `json:"user"` +} diff --git a/enterprise/audit/audit.go b/enterprise/audit/audit.go new file mode 100644 index 0000000000000..f4a27962103cd --- /dev/null +++ b/enterprise/audit/audit.go @@ -0,0 +1,39 @@ +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 +} + +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 +} diff --git a/enterprise/audit/diff.go b/enterprise/audit/diff.go new file mode 100644 index 0000000000000..7602826bd30cc --- /dev/null +++ b/enterprise/audit/diff.go @@ -0,0 +1,164 @@ +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(left, right any, 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() + } + + 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(), + Secret: true, + } + } + } + } + + return baseDiff +} + +// convertDiffType converts external struct types to primitive types. +// +//nolint:forcetypeassert +func convertDiffType(left, right any) (newLeft, newRight any, changed bool) { + switch typedLeft := left.(type) { + case uuid.UUID: + 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, _ := typedLeft.MarshalText() + rightStr, _ := right.(uuid.NullUUID).MarshalText() + return string(leftStr), string(rightStr), true + + case sql.NullString: + leftStr := typedLeft.String + if !typedLeft.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 !typedLeft.Valid { + leftInt64Ptr = nil + } else { + leftInt64Ptr = ptr(typedLeft.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/enterprise/audit/diff_internal_test.go b/enterprise/audit/diff_internal_test.go new file mode 100644 index 0000000000000..bdc4e87b7c30f --- /dev/null +++ b/enterprise/audit/diff_internal_test.go @@ -0,0 +1,396 @@ +package audit + +import ( + "database/sql" + "reflect" + "testing" + "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 Test_diffValues(t *testing.T) { + t.Parallel() + + t.Run("Normal", func(t *testing.T) { + t.Parallel() + + type foo struct { + Bar string `json:"bar"` + Baz int `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"}, + }, + }, + }) + }) + + //nolint:revive + 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: ""}, + }, + }, + }) + }) + + // We currently don't support nested structs. + // 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](), + right: database.GitSSHKey{ + UserID: uuid.UUID{1}, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + PrivateKey: "a very secret private key", + PublicKey: "a very public public key", + }, + exp: audit.Map{ + "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"}, + }, + }, + }) + + runDiffTests(t, []diffTest{ + { + name: "Create", + left: audit.Empty[database.OrganizationMember](), + right: database.OrganizationMember{ + UserID: uuid.UUID{1}, + OrganizationID: uuid.UUID{2}, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + Roles: []string{"auditor"}, + }, + exp: audit.Map{ + "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"}}, + }, + }, + }) + + runDiffTests(t, []diffTest{ + { + name: "Create", + left: audit.Empty[database.Organization](), + right: database.Organization{ + ID: uuid.UUID{1}, + Name: "rust developers", + Description: "an organization for rust developers", + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + }, + exp: audit.Map{ + "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"}, + }, + }, + }) + + runDiffTests(t, []diffTest{ + { + name: "Create", + left: audit.Empty[database.Template](), + right: database.Template{ + ID: uuid.UUID{1}, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + OrganizationID: uuid.UUID{2}, + Deleted: false, + Name: "rust", + Provisioner: database.ProvisionerTypeTerraform, + ActiveVersionID: uuid.UUID{3}, + MaxTtl: int64(time.Hour), + MinAutostartInterval: int64(time.Minute), + CreatedBy: uuid.UUID{4}, + }, + exp: audit.Map{ + "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()}, + }, + }, + }) + + runDiffTests(t, []diffTest{ + { + name: "Create", + left: audit.Empty[database.TemplateVersion](), + right: database.TemplateVersion{ + ID: uuid.UUID{1}, + TemplateID: uuid.NullUUID{UUID: uuid.UUID{2}, Valid: true}, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + OrganizationID: uuid.UUID{3}, + Name: "rust", + CreatedBy: uuid.NullUUID{UUID: uuid.UUID{4}, Valid: true}, + }, + exp: audit.Map{ + "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"}, + }, + }, + { + name: "CreateNullTemplateID", + left: audit.Empty[database.TemplateVersion](), + right: database.TemplateVersion{ + ID: uuid.UUID{1}, + TemplateID: uuid.NullUUID{}, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + OrganizationID: uuid.UUID{3}, + Name: "rust", + CreatedBy: uuid.NullUUID{UUID: uuid.UUID{4}, Valid: true}, + }, + exp: audit.Map{ + "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"}, + }, + }, + }) + + runDiffTests(t, []diffTest{ + { + name: "Create", + left: audit.Empty[database.User](), + right: database.User{ + ID: uuid.UUID{1}, + Email: "colin@coder.com", + Username: "colin", + HashedPassword: []byte("hunter2ButHashed"), + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + Status: database.UserStatusActive, + RBACRoles: []string{"omega admin"}, + }, + exp: audit.Map{ + "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"}}, + }, + }, + }) + + runDiffTests(t, []diffTest{ + { + name: "Create", + left: audit.Empty[database.Workspace](), + right: database.Workspace{ + ID: uuid.UUID{1}, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + OwnerID: uuid.UUID{2}, + TemplateID: uuid.UUID{3}, + Name: "rust workspace", + AutostartSchedule: sql.NullString{String: "0 12 * * 1-5", Valid: true}, + Ttl: sql.NullInt64{Int64: int64(8 * time.Hour), Valid: true}, + }, + exp: audit.Map{ + "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 + }, + }, + { + name: "NullSchedules", + left: audit.Empty[database.Workspace](), + right: database.Workspace{ + ID: uuid.UUID{1}, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + OwnerID: uuid.UUID{2}, + TemplateID: uuid.UUID{3}, + Name: "rust workspace", + AutostartSchedule: sql.NullString{}, + Ttl: sql.NullInt64{}, + }, + exp: audit.Map{ + "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"}, + }, + }, + }) +} + +func runDiffTests(t *testing.T, tests []diffTest) { + t.Helper() + + 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, + diffValues(test.left, test.right, AuditableResources), + ) + }) + } +} 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 02bef91d52bcb..9f75796da19bd 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 14f1d5abb8366..655787e0abb72 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -34,6 +34,41 @@ export interface AssignableRoles extends Role { readonly assignable: boolean } +// From codersdk/audit.go +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 { + 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 resource_icon: 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 +} + // From codersdk/users.go export interface AuthMethods { readonly password: boolean @@ -576,6 +611,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" @@ -618,6 +656,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"