From 49b69b4a65c44a904fb291befe9e07459165a698 Mon Sep 17 00:00:00 2001 From: Colin Adler Date: Thu, 5 May 2022 17:04:24 -0500 Subject: [PATCH 1/5] feat: add audit exporting and filtering --- coderd/audit/backends/postgres.go | 53 +++++++ coderd/audit/backends/postgres_test.go | 65 +++++++++ coderd/audit/backends/slog.go | 34 +++++ coderd/audit/backends/slog_test.go | 46 ++++++ coderd/audit/exporter.go | 51 +++++++ coderd/audit/exporter_test.go | 131 ++++++++++++++++++ coderd/audit/filter.go | 40 ++++++ coderd/database/databasefake/databasefake.go | 13 +- coderd/database/dump.sql | 2 +- .../migrations/000010_audit_logs.up.sql | 2 +- coderd/database/models.go | 2 +- coderd/database/queries.sql.go | 13 +- coderd/database/queries/auditlogs.sql | 2 +- go.mod | 1 + go.sum | 1 + 15 files changed, 442 insertions(+), 14 deletions(-) create mode 100644 coderd/audit/backends/postgres.go create mode 100644 coderd/audit/backends/postgres_test.go create mode 100644 coderd/audit/backends/slog.go create mode 100644 coderd/audit/backends/slog_test.go create mode 100644 coderd/audit/exporter.go create mode 100644 coderd/audit/exporter_test.go create mode 100644 coderd/audit/filter.go diff --git a/coderd/audit/backends/postgres.go b/coderd/audit/backends/postgres.go new file mode 100644 index 0000000000000..b176402c615de --- /dev/null +++ b/coderd/audit/backends/postgres.go @@ -0,0 +1,53 @@ +package backends + +import ( + "context" + + "golang.org/x/xerrors" + + "github.com/coder/coder/coderd/audit" + "github.com/coder/coder/coderd/database" +) + +type pgBackend struct { + // internal indicates if the exporter is exporting to the Postgres database + // that the rest of Coderd uses. Since this is a generic Postgres exporter, + // we make different decisions to store the audit log based on if it's + // pointing to the Coderd database. + internal bool + db database.Store +} + +func NewPGBackend(db database.Store, internal bool) audit.Backend { + return &pgBackend{db: db, internal: internal} +} + +func (b *pgBackend) Decision() audit.FilterDecision { + if b.internal { + return audit.FilterDecisionStore + } + + return audit.FilterDecisionExport +} + +func (b *pgBackend) Export(ctx context.Context, alog database.AuditLog) error { + _, err := b.db.InsertAuditLog(ctx, database.InsertAuditLogParams{ + ID: alog.ID, + Time: alog.Time, + UserID: alog.UserID, + OrganizationID: alog.OrganizationID, + Ip: alog.Ip, + UserAgent: alog.UserAgent, + ResourceType: alog.ResourceType, + ResourceID: alog.ResourceID, + ResourceTarget: alog.ResourceTarget, + Action: alog.Action, + Diff: alog.Diff, + StatusCode: alog.StatusCode, + }) + if err != nil { + return xerrors.Errorf("insert audit log: %w", err) + } + + return nil +} diff --git a/coderd/audit/backends/postgres_test.go b/coderd/audit/backends/postgres_test.go new file mode 100644 index 0000000000000..9f940d6acf4cd --- /dev/null +++ b/coderd/audit/backends/postgres_test.go @@ -0,0 +1,65 @@ +package backends_test + +import ( + "context" + "net" + "net/http" + "testing" + "time" + + "github.com/google/uuid" + "github.com/stretchr/testify/require" + "github.com/tabbed/pqtype" + + "github.com/coder/coder/coderd/audit/backends" + "github.com/coder/coder/coderd/database" + "github.com/coder/coder/coderd/database/databasefake" +) + +func TestPostgresBackend(t *testing.T) { + t.Parallel() + t.Run("OK", func(t *testing.T) { + t.Parallel() + + var ( + ctx, cancel = context.WithCancel(context.Background()) + db = databasefake.New() + pgb = backends.NewPGBackend(db, true) + alog = randomAuditLog() + ) + defer cancel() + + err := pgb.Export(ctx, alog) + require.NoError(t, err) + + got, err := db.GetAuditLogsBefore(ctx, database.GetAuditLogsBeforeParams{ + ID: uuid.Nil, + StartTime: time.Now().Add(time.Second), + RowLimit: 1, + }) + require.NoError(t, err) + require.Len(t, got, 1) + require.Equal(t, alog, got[0]) + }) +} + +func randomAuditLog() database.AuditLog { + _, inet, _ := net.ParseCIDR("127.0.0.1/32") + return database.AuditLog{ + ID: uuid.New(), + Time: time.Now(), + UserID: uuid.New(), + OrganizationID: uuid.New(), + Ip: pqtype.Inet{ + IPNet: *inet, + Valid: true, + }, + UserAgent: "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Safari/537.36", + ResourceType: database.ResourceTypeOrganization, + ResourceID: uuid.New(), + ResourceTarget: "colin's organization", + Action: database.AuditActionDelete, + Diff: []byte{}, + StatusCode: http.StatusNoContent, + } +} diff --git a/coderd/audit/backends/slog.go b/coderd/audit/backends/slog.go new file mode 100644 index 0000000000000..54f2ed971e2b8 --- /dev/null +++ b/coderd/audit/backends/slog.go @@ -0,0 +1,34 @@ +package backends + +import ( + "context" + + "github.com/fatih/structs" + + "cdr.dev/slog" + "github.com/coder/coder/coderd/audit" + "github.com/coder/coder/coderd/database" +) + +type slogBackend struct { + log slog.Logger +} + +func NewSlogBackend(logger slog.Logger) audit.Backend { + return slogBackend{log: logger} +} + +func (slogBackend) Decision() audit.FilterDecision { + return audit.FilterDecisionExport +} + +func (b slogBackend) Export(ctx context.Context, alog database.AuditLog) error { + m := structs.Map(alog) + fields := make([]slog.Field, 0, len(m)) + for k, v := range m { + fields = append(fields, slog.F(k, v)) + } + + b.log.Info(ctx, "audit_log", fields...) + return nil +} diff --git a/coderd/audit/backends/slog_test.go b/coderd/audit/backends/slog_test.go new file mode 100644 index 0000000000000..78ddab72a0870 --- /dev/null +++ b/coderd/audit/backends/slog_test.go @@ -0,0 +1,46 @@ +package backends_test + +import ( + "context" + "testing" + + "github.com/fatih/structs" + "github.com/stretchr/testify/require" + + "cdr.dev/slog" + "github.com/coder/coder/coderd/audit/backends" +) + +func TestSlogBackend(t *testing.T) { + t.Parallel() + t.Run("OK", func(t *testing.T) { + t.Parallel() + + var ( + ctx, cancel = context.WithCancel(context.Background()) + + sink = &fakeSink{} + logger = slog.Make(sink) + backend = backends.NewSlogBackend(logger) + + alog = randomAuditLog() + ) + defer cancel() + + err := backend.Export(ctx, alog) + require.NoError(t, err) + require.Len(t, sink.entries, 1) + require.Equal(t, sink.entries[0].Message, "audit_log") + require.Len(t, sink.entries[0].Fields, len(structs.Fields(alog))) + }) +} + +type fakeSink struct { + entries []slog.SinkEntry +} + +func (s *fakeSink) LogEntry(_ context.Context, e slog.SinkEntry) { + s.entries = append(s.entries, e) +} + +func (*fakeSink) Sync() {} diff --git a/coderd/audit/exporter.go b/coderd/audit/exporter.go new file mode 100644 index 0000000000000..ea56b0f5db6f5 --- /dev/null +++ b/coderd/audit/exporter.go @@ -0,0 +1,51 @@ +package audit + +import ( + "context" + + "golang.org/x/xerrors" + + "github.com/coder/coder/coderd/database" +) + +// Backends can accept and store or send audit logs elsewhere. +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 +} + +// Exporter exports audit logs to an arbitrary amount of backends. +type Exporter struct { + filter Filter + backends []Backend +} + +func NewExporter(filter Filter, backends ...Backend) *Exporter { + return &Exporter{ + filter: filter, + backends: backends, + } +} + +func (e *Exporter) Export(ctx context.Context, alog database.AuditLog) error { + for _, backend := range e.backends { + decision, err := e.filter.Check(ctx, alog) + if err != nil { + return xerrors.Errorf("filter check: %w", err) + } + + if decision&backend.Decision() != backend.Decision() { + continue + } + + err = backend.Export(ctx, alog) + if err != nil { + // naively return the first error. should probably make this smarter + // by returning multiple errors. + return xerrors.Errorf("export audit log to backend: %w", err) + } + } + return nil +} diff --git a/coderd/audit/exporter_test.go b/coderd/audit/exporter_test.go new file mode 100644 index 0000000000000..39376b4775558 --- /dev/null +++ b/coderd/audit/exporter_test.go @@ -0,0 +1,131 @@ +package audit_test + +import ( + "context" + "net" + "net/http" + "testing" + "time" + + "github.com/google/uuid" + "github.com/stretchr/testify/require" + "github.com/tabbed/pqtype" + + "github.com/coder/coder/coderd/audit" + "github.com/coder/coder/coderd/database" +) + +func TestExporter(t *testing.T) { + t.Parallel() + + var tests = []struct { + name string + filterDecision audit.FilterDecision + backendDecision audit.FilterDecision + shouldExport bool + }{ + { + name: "ShouldDrop", + filterDecision: audit.FilterDecisionDrop, + backendDecision: audit.FilterDecisionStore, + shouldExport: false, + }, + { + name: "ShouldStore", + filterDecision: audit.FilterDecisionStore, + backendDecision: audit.FilterDecisionStore, + shouldExport: true, + }, + { + name: "ShouldNotStore", + filterDecision: audit.FilterDecisionExport, + backendDecision: audit.FilterDecisionStore, + shouldExport: false, + }, + { + name: "ShouldExport", + filterDecision: audit.FilterDecisionExport, + backendDecision: audit.FilterDecisionExport, + shouldExport: true, + }, + { + name: "ShouldNotExport", + filterDecision: audit.FilterDecisionStore, + backendDecision: audit.FilterDecisionExport, + shouldExport: false, + }, + { + name: "ShouldStoreOrExport", + filterDecision: audit.FilterDecisionStore | audit.FilterDecisionExport, + backendDecision: audit.FilterDecisionExport, + shouldExport: true, + }, + // When more filters are written they should have their own tests. + { + name: "DefaultFilter", + filterDecision: func() audit.FilterDecision { + decision, _ := audit.DefaultFilter.Check(context.Background(), randomAuditLog()) + return decision + }(), + backendDecision: audit.FilterDecisionExport, + shouldExport: true, + }, + } + + for _, test := range tests { + test := test + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + var ( + backend = &testBackend{decision: test.backendDecision} + exporter = audit.NewExporter( + audit.FilterFunc(func(_ context.Context, _ database.AuditLog) (audit.FilterDecision, error) { + return test.filterDecision, nil + }), + backend, + ) + ) + + err := exporter.Export(context.Background(), randomAuditLog()) + require.NoError(t, err) + require.Equal(t, len(backend.alogs) > 0, test.shouldExport) + }) + } +} + +func randomAuditLog() database.AuditLog { + _, inet, _ := net.ParseCIDR("127.0.0.1/32") + return database.AuditLog{ + ID: uuid.New(), + Time: time.Now(), + UserID: uuid.New(), + OrganizationID: uuid.New(), + Ip: pqtype.Inet{ + IPNet: *inet, + Valid: true, + }, + UserAgent: "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Safari/537.36", + ResourceType: database.ResourceTypeOrganization, + ResourceID: uuid.New(), + ResourceTarget: "colin's organization", + Action: database.AuditActionDelete, + Diff: []byte{}, + StatusCode: http.StatusNoContent, + } +} + +type testBackend struct { + decision audit.FilterDecision + + alogs []database.AuditLog +} + +func (t *testBackend) Decision() audit.FilterDecision { + return t.decision +} + +func (t *testBackend) Export(_ context.Context, alog database.AuditLog) error { + t.alogs = append(t.alogs, alog) + return nil +} diff --git a/coderd/audit/filter.go b/coderd/audit/filter.go new file mode 100644 index 0000000000000..b00a4887754ca --- /dev/null +++ b/coderd/audit/filter.go @@ -0,0 +1,40 @@ +package audit + +import ( + "context" + + "github.com/coder/coder/coderd/database" +) + +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/database/databasefake/databasefake.go b/coderd/database/databasefake/databasefake.go index 5726c9f7befe5..ce7747f8d5dab 100644 --- a/coderd/database/databasefake/databasefake.go +++ b/coderd/database/databasefake/databasefake.go @@ -1629,11 +1629,16 @@ func (q *fakeQuerier) GetAuditLogsBefore(_ context.Context, arg database.GetAudi logs := make([]database.AuditLog, 0) start := database.AuditLog{} - for _, alog := range q.auditLogs { - if alog.ID == arg.ID { - start = alog - break + if arg.ID != uuid.Nil { + for _, alog := range q.auditLogs { + if alog.ID == arg.ID { + start = alog + break + } } + } else { + start.ID = uuid.New() + start.Time = arg.StartTime } if start.ID == uuid.Nil { diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index dd18186ea761b..46cfc646e8158 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -101,7 +101,7 @@ CREATE TABLE audit_logs ( "time" timestamp with time zone NOT NULL, user_id uuid NOT NULL, organization_id uuid NOT NULL, - ip cidr NOT NULL, + ip inet NOT NULL, user_agent character varying(256) NOT NULL, resource_type resource_type NOT NULL, resource_id uuid NOT NULL, diff --git a/coderd/database/migrations/000010_audit_logs.up.sql b/coderd/database/migrations/000010_audit_logs.up.sql index 7149679a44947..6c5e8dff9f075 100644 --- a/coderd/database/migrations/000010_audit_logs.up.sql +++ b/coderd/database/migrations/000010_audit_logs.up.sql @@ -18,7 +18,7 @@ CREATE TABLE audit_logs ( "time" timestamp with time zone NOT NULL, user_id uuid NOT NULL, organization_id uuid NOT NULL, - ip cidr NOT NULL, + ip inet NOT NULL, user_agent varchar(256) NOT NULL, resource_type resource_type NOT NULL, resource_id uuid NOT NULL, diff --git a/coderd/database/models.go b/coderd/database/models.go index 0fd6e4ca28270..ad7277e32cbbe 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -311,7 +311,7 @@ type AuditLog struct { 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.CIDR `db:"ip" json:"ip"` + 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"` diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index f91070915c097..199703bbe5b8e 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -154,22 +154,23 @@ SELECT FROM audit_logs WHERE - "time" < (SELECT "time" FROM audit_logs a WHERE a.id = $1) + audit_logs."time" < COALESCE((SELECT "time" FROM audit_logs a WHERE a.id = $1), $2) ORDER BY "time" DESC LIMIT - $2 + $3 ` type GetAuditLogsBeforeParams struct { - ID uuid.UUID `db:"id" json:"id"` - RowLimit int32 `db:"row_limit" json:"row_limit"` + ID uuid.UUID `db:"id" json:"id"` + StartTime time.Time `db:"start_time" json:"start_time"` + RowLimit int32 `db:"row_limit" json:"row_limit"` } // GetAuditLogsBefore retrieves `limit` number of audit logs before the provided // ID. func (q *sqlQuerier) GetAuditLogsBefore(ctx context.Context, arg GetAuditLogsBeforeParams) ([]AuditLog, error) { - rows, err := q.db.QueryContext(ctx, getAuditLogsBefore, arg.ID, arg.RowLimit) + rows, err := q.db.QueryContext(ctx, getAuditLogsBefore, arg.ID, arg.StartTime, arg.RowLimit) if err != nil { return nil, err } @@ -229,7 +230,7 @@ type InsertAuditLogParams struct { 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.CIDR `db:"ip" json:"ip"` + 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"` diff --git a/coderd/database/queries/auditlogs.sql b/coderd/database/queries/auditlogs.sql index 90ec14e526a7f..e827ea1cd113f 100644 --- a/coderd/database/queries/auditlogs.sql +++ b/coderd/database/queries/auditlogs.sql @@ -6,7 +6,7 @@ SELECT FROM audit_logs WHERE - "time" < (SELECT "time" FROM audit_logs a WHERE a.id = sqlc.arg(id)) + audit_logs."time" < COALESCE((SELECT "time" FROM audit_logs a WHERE a.id = sqlc.arg(id)), sqlc.arg(start_time)) ORDER BY "time" DESC LIMIT diff --git a/go.mod b/go.mod index 128a705b78d79..b7c27ea1d1797 100644 --- a/go.mod +++ b/go.mod @@ -155,6 +155,7 @@ require ( github.com/dustin/go-humanize v1.0.0 // indirect github.com/fatedier/beego v0.0.0-20171024143340-6c6a4f5bd5eb // indirect github.com/fatedier/kcp-go v2.0.4-0.20190803094908-fe8645b0a904+incompatible // indirect + github.com/fatih/structs v1.1.0 // indirect github.com/ghodss/yaml v1.0.0 // indirect github.com/go-playground/locales v0.14.0 // indirect github.com/go-playground/universal-translator v0.18.0 // indirect diff --git a/go.sum b/go.sum index 5340761d66be1..17a312897dcc3 100644 --- a/go.sum +++ b/go.sum @@ -579,6 +579,7 @@ github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL github.com/fatih/color v1.12.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/felixge/httpsnoop v1.0.2/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= From a60c1f7263e0ec79567995b814f1380005ce62af Mon Sep 17 00:00:00 2001 From: Colin Adler Date: Mon, 9 May 2022 16:48:06 -0500 Subject: [PATCH 2/5] backend renaming and more docs --- coderd/audit/backends/postgres.go | 10 +++++----- coderd/audit/backends/postgres_test.go | 2 +- coderd/audit/backends/slog.go | 2 +- coderd/audit/backends/slog_test.go | 2 +- coderd/audit/exporter.go | 8 ++++++-- 5 files changed, 14 insertions(+), 10 deletions(-) diff --git a/coderd/audit/backends/postgres.go b/coderd/audit/backends/postgres.go index b176402c615de..db02580c8e1d0 100644 --- a/coderd/audit/backends/postgres.go +++ b/coderd/audit/backends/postgres.go @@ -9,7 +9,7 @@ import ( "github.com/coder/coder/coderd/database" ) -type pgBackend struct { +type postgresBackend struct { // internal indicates if the exporter is exporting to the Postgres database // that the rest of Coderd uses. Since this is a generic Postgres exporter, // we make different decisions to store the audit log based on if it's @@ -18,11 +18,11 @@ type pgBackend struct { db database.Store } -func NewPGBackend(db database.Store, internal bool) audit.Backend { - return &pgBackend{db: db, internal: internal} +func NewPostgres(db database.Store, internal bool) audit.Backend { + return &postgresBackend{db: db, internal: internal} } -func (b *pgBackend) Decision() audit.FilterDecision { +func (b *postgresBackend) Decision() audit.FilterDecision { if b.internal { return audit.FilterDecisionStore } @@ -30,7 +30,7 @@ func (b *pgBackend) Decision() audit.FilterDecision { return audit.FilterDecisionExport } -func (b *pgBackend) Export(ctx context.Context, alog database.AuditLog) error { +func (b *postgresBackend) Export(ctx context.Context, alog database.AuditLog) error { _, err := b.db.InsertAuditLog(ctx, database.InsertAuditLogParams{ ID: alog.ID, Time: alog.Time, diff --git a/coderd/audit/backends/postgres_test.go b/coderd/audit/backends/postgres_test.go index 9f940d6acf4cd..952443cd67cae 100644 --- a/coderd/audit/backends/postgres_test.go +++ b/coderd/audit/backends/postgres_test.go @@ -24,7 +24,7 @@ func TestPostgresBackend(t *testing.T) { var ( ctx, cancel = context.WithCancel(context.Background()) db = databasefake.New() - pgb = backends.NewPGBackend(db, true) + pgb = backends.NewPostgres(db, true) alog = randomAuditLog() ) defer cancel() diff --git a/coderd/audit/backends/slog.go b/coderd/audit/backends/slog.go index 54f2ed971e2b8..ad9191a0c6c92 100644 --- a/coderd/audit/backends/slog.go +++ b/coderd/audit/backends/slog.go @@ -14,7 +14,7 @@ type slogBackend struct { log slog.Logger } -func NewSlogBackend(logger slog.Logger) audit.Backend { +func NewSlog(logger slog.Logger) audit.Backend { return slogBackend{log: logger} } diff --git a/coderd/audit/backends/slog_test.go b/coderd/audit/backends/slog_test.go index 78ddab72a0870..428637dd4b5a0 100644 --- a/coderd/audit/backends/slog_test.go +++ b/coderd/audit/backends/slog_test.go @@ -21,7 +21,7 @@ func TestSlogBackend(t *testing.T) { sink = &fakeSink{} logger = slog.Make(sink) - backend = backends.NewSlogBackend(logger) + backend = backends.NewSlog(logger) alog = randomAuditLog() ) diff --git a/coderd/audit/exporter.go b/coderd/audit/exporter.go index ea56b0f5db6f5..de61e88fa9f4d 100644 --- a/coderd/audit/exporter.go +++ b/coderd/audit/exporter.go @@ -8,7 +8,7 @@ import ( "github.com/coder/coder/coderd/database" ) -// Backends can accept and store or send audit logs elsewhere. +// Backends can store or send audit logs to arbitrary locations. type Backend interface { // Decision determines the FilterDecisions that the backend tolerates. Decision() FilterDecision @@ -16,12 +16,13 @@ type Backend interface { Export(ctx context.Context, alog database.AuditLog) error } -// Exporter exports audit logs to an arbitrary amount of backends. +// Exporter exports audit logs to an arbitrary list of backends. type Exporter struct { filter Filter backends []Backend } +// NewExporter creates an exporter from the given filter and backends. func NewExporter(filter Filter, backends ...Backend) *Exporter { return &Exporter{ filter: filter, @@ -29,6 +30,9 @@ func NewExporter(filter Filter, backends ...Backend) *Exporter { } } +// Export exports and audit log. Before exporting to a backend, it uses the +// filter to determine if the backend tolerates the audit log. If not, it is +// dropped. func (e *Exporter) Export(ctx context.Context, alog database.AuditLog) error { for _, backend := range e.backends { decision, err := e.filter.Check(ctx, alog) From 9cff344ea44a4dd474afbf0b0d90a3b8220a5151 Mon Sep 17 00:00:00 2001 From: Colin Adler Date: Mon, 9 May 2022 16:49:01 -0500 Subject: [PATCH 3/5] only check filter once on export --- coderd/audit/exporter.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/coderd/audit/exporter.go b/coderd/audit/exporter.go index de61e88fa9f4d..c1d801c4a5bec 100644 --- a/coderd/audit/exporter.go +++ b/coderd/audit/exporter.go @@ -34,11 +34,12 @@ func NewExporter(filter Filter, backends ...Backend) *Exporter { // filter to determine if the backend tolerates the audit log. If not, it is // dropped. func (e *Exporter) Export(ctx context.Context, alog database.AuditLog) error { + decision, err := e.filter.Check(ctx, alog) + if err != nil { + return xerrors.Errorf("filter check: %w", err) + } + for _, backend := range e.backends { - decision, err := e.filter.Check(ctx, alog) - if err != nil { - return xerrors.Errorf("filter check: %w", err) - } if decision&backend.Decision() != backend.Decision() { continue From b001f7666eb049f8c334541e168e50bb8decf7dd Mon Sep 17 00:00:00 2001 From: Colin Adler Date: Mon, 9 May 2022 16:50:52 -0500 Subject: [PATCH 4/5] document FilterDecision --- coderd/audit/filter.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/coderd/audit/filter.go b/coderd/audit/filter.go index b00a4887754ca..868d5bb7d77db 100644 --- a/coderd/audit/filter.go +++ b/coderd/audit/filter.go @@ -6,6 +6,8 @@ import ( "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 ( From 435940a181d881e55ba7bcfdc71f5b0c49de0051 Mon Sep 17 00:00:00 2001 From: Colin Adler Date: Mon, 9 May 2022 16:54:27 -0500 Subject: [PATCH 5/5] fix lint --- coderd/audit/exporter.go | 1 - 1 file changed, 1 deletion(-) diff --git a/coderd/audit/exporter.go b/coderd/audit/exporter.go index c1d801c4a5bec..c9b37aec5fb62 100644 --- a/coderd/audit/exporter.go +++ b/coderd/audit/exporter.go @@ -40,7 +40,6 @@ func (e *Exporter) Export(ctx context.Context, alog database.AuditLog) error { } for _, backend := range e.backends { - if decision&backend.Decision() != backend.Decision() { continue }