Skip to content

Commit 796a975

Browse files
authored
feat(enterprise/audit): add user object to slog exporter (#9456)
1 parent 11d4b6f commit 796a975

File tree

9 files changed

+71
-24
lines changed

9 files changed

+71
-24
lines changed

coderd/audit/request.go

+6-4
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,14 @@ type Request[T Auditable] struct {
3333
Old T
3434
New T
3535

36-
// This optional field can be passed in when the userID cannot be determined from the API Key
37-
// such as in the case of login, when the audit log is created prior the API Key's existence.
36+
// UserID is an optional field can be passed in when the userID cannot be
37+
// determined from the API Key such as in the case of login, when the audit
38+
// log is created prior the API Key's existence.
3839
UserID uuid.UUID
3940

40-
// This optional field can be passed in if the AuditAction must be overridden
41-
// such as in the case of new user authentication when the Audit Action is 'register', not 'login'.
41+
// Action is an optional field can be passed in if the AuditAction must be
42+
// overridden such as in the case of new user authentication when the Audit
43+
// Action is 'register', not 'login'.
4244
Action database.AuditAction
4345
}
4446

enterprise/audit/audit.go

+27-3
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,37 @@ package audit
22

33
import (
44
"context"
5+
"database/sql"
56

7+
"github.com/google/uuid"
68
"golang.org/x/xerrors"
79

810
"github.com/coder/coder/v2/coderd/audit"
911
"github.com/coder/coder/v2/coderd/database"
12+
"github.com/coder/coder/v2/coderd/database/dbauthz"
1013
)
1114

15+
type BackendDetails struct {
16+
Actor *Actor
17+
}
18+
19+
type Actor struct {
20+
ID uuid.UUID `json:"id"`
21+
Email string `json:"email"`
22+
Username string `json:"username"`
23+
}
24+
1225
// Backends can store or send audit logs to arbitrary locations.
1326
type Backend interface {
1427
// Decision determines the FilterDecisions that the backend tolerates.
1528
Decision() FilterDecision
1629
// Export sends an audit log to the backend.
17-
Export(ctx context.Context, alog database.AuditLog) error
30+
Export(ctx context.Context, alog database.AuditLog, details BackendDetails) error
1831
}
1932

20-
func NewAuditor(filter Filter, backends ...Backend) audit.Auditor {
33+
func NewAuditor(db database.Store, filter Filter, backends ...Backend) audit.Auditor {
2134
return &auditor{
35+
db: db,
2236
filter: filter,
2337
backends: backends,
2438
Differ: audit.Differ{DiffFn: func(old, new any) audit.Map {
@@ -29,6 +43,7 @@ func NewAuditor(filter Filter, backends ...Backend) audit.Auditor {
2943

3044
// auditor is the enterprise implementation of the Auditor interface.
3145
type auditor struct {
46+
db database.Store
3247
filter Filter
3348
backends []Backend
3449

@@ -41,12 +56,21 @@ func (a *auditor) Export(ctx context.Context, alog database.AuditLog) error {
4156
return xerrors.Errorf("filter check: %w", err)
4257
}
4358

59+
actor, err := a.db.GetUserByID(dbauthz.AsSystemRestricted(ctx), alog.UserID) //nolint
60+
if err != nil && !xerrors.Is(err, sql.ErrNoRows) {
61+
return err
62+
}
63+
4464
for _, backend := range a.backends {
4565
if decision&backend.Decision() != backend.Decision() {
4666
continue
4767
}
4868

49-
err = backend.Export(ctx, alog)
69+
err = backend.Export(ctx, alog, BackendDetails{Actor: &Actor{
70+
ID: actor.ID,
71+
Email: actor.Email,
72+
Username: actor.Username,
73+
}})
5074
if err != nil {
5175
// naively return the first error. should probably make this smarter
5276
// by returning multiple errors.

enterprise/audit/audit_test.go

+13-3
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"golang.org/x/xerrors"
99

1010
"github.com/coder/coder/v2/coderd/database"
11+
"github.com/coder/coder/v2/coderd/database/dbfake"
1112
"github.com/coder/coder/v2/enterprise/audit"
1213
"github.com/coder/coder/v2/enterprise/audit/audittest"
1314
)
@@ -90,6 +91,7 @@ func TestAuditor(t *testing.T) {
9091
var (
9192
backend = &testBackend{decision: test.backendDecision, err: test.backendError}
9293
exporter = audit.NewAuditor(
94+
dbfake.New(),
9395
audit.FilterFunc(func(_ context.Context, _ database.AuditLog) (audit.FilterDecision, error) {
9496
return test.filterDecision, test.filterError
9597
}),
@@ -113,18 +115,26 @@ type testBackend struct {
113115
decision audit.FilterDecision
114116
err error
115117

116-
alogs []database.AuditLog
118+
alogs []testExport
119+
}
120+
121+
type testExport struct {
122+
alog database.AuditLog
123+
details audit.BackendDetails
117124
}
118125

119126
func (t *testBackend) Decision() audit.FilterDecision {
120127
return t.decision
121128
}
122129

123-
func (t *testBackend) Export(_ context.Context, alog database.AuditLog) error {
130+
func (t *testBackend) Export(_ context.Context, alog database.AuditLog, details audit.BackendDetails) error {
124131
if t.err != nil {
125132
return t.err
126133
}
127134

128-
t.alogs = append(t.alogs, alog)
135+
t.alogs = append(t.alogs, testExport{
136+
alog: alog,
137+
details: details,
138+
})
129139
return nil
130140
}

enterprise/audit/backends/postgres.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ func (b *postgresBackend) Decision() audit.FilterDecision {
3030
return audit.FilterDecisionExport
3131
}
3232

33-
func (b *postgresBackend) Export(ctx context.Context, alog database.AuditLog) error {
33+
func (b *postgresBackend) Export(ctx context.Context, alog database.AuditLog, _ audit.BackendDetails) error {
3434
_, err := b.db.InsertAuditLog(ctx, database.InsertAuditLogParams(alog))
3535
if err != nil {
3636
return xerrors.Errorf("insert audit log: %w", err)

enterprise/audit/backends/postgres_test.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88

99
"github.com/coder/coder/v2/coderd/database"
1010
"github.com/coder/coder/v2/coderd/database/dbfake"
11+
"github.com/coder/coder/v2/enterprise/audit"
1112
"github.com/coder/coder/v2/enterprise/audit/audittest"
1213
"github.com/coder/coder/v2/enterprise/audit/backends"
1314
)
@@ -25,7 +26,7 @@ func TestPostgresBackend(t *testing.T) {
2526
)
2627
defer cancel()
2728

28-
err := pgb.Export(ctx, alog)
29+
err := pgb.Export(ctx, alog, audit.BackendDetails{})
2930
require.NoError(t, err)
3031

3132
got, err := db.GetAuditLogsOffset(ctx, database.GetAuditLogsOffsetParams{

enterprise/audit/backends/slog.go

+5-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ func (*slogBackend) Decision() audit.FilterDecision {
2424
return audit.FilterDecisionExport
2525
}
2626

27-
func (b *slogBackend) Export(ctx context.Context, alog database.AuditLog) error {
27+
func (b *slogBackend) Export(ctx context.Context, alog database.AuditLog, details audit.BackendDetails) error {
2828
// We don't use structs.Map because we don't want to recursively convert
2929
// fields into maps. When we keep the type information, slog can more
3030
// pleasantly format the output. For example, the clean result of
@@ -35,6 +35,10 @@ func (b *slogBackend) Export(ctx context.Context, alog database.AuditLog) error
3535
fields = append(fields, b.fieldToSlog(sf))
3636
}
3737

38+
if details.Actor != nil {
39+
fields = append(fields, slog.F("actor", details.Actor))
40+
}
41+
3842
b.log.Info(ctx, "audit_log", fields...)
3943
return nil
4044
}

enterprise/audit/backends/slog_test.go

+9-4
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"cdr.dev/slog"
2020
"cdr.dev/slog/sloggers/slogjson"
2121
"github.com/coder/coder/v2/coderd/database"
22+
"github.com/coder/coder/v2/enterprise/audit"
2223
"github.com/coder/coder/v2/enterprise/audit/audittest"
2324
"github.com/coder/coder/v2/enterprise/audit/backends"
2425
)
@@ -39,7 +40,7 @@ func TestSlogBackend(t *testing.T) {
3940
)
4041
defer cancel()
4142

42-
err := backend.Export(ctx, alog)
43+
err := backend.Export(ctx, alog, audit.BackendDetails{})
4344
require.NoError(t, err)
4445
require.Len(t, sink.entries, 1)
4546
require.Equal(t, sink.entries[0].Message, "audit_log")
@@ -59,7 +60,7 @@ func TestSlogBackend(t *testing.T) {
5960
_, inet, _ = net.ParseCIDR("127.0.0.1/32")
6061
alog = database.AuditLog{
6162
ID: uuid.UUID{1},
62-
Time: time.Unix(1257894000, 0),
63+
Time: time.Unix(1257894000, 0).UTC(),
6364
UserID: uuid.UUID{2},
6465
OrganizationID: uuid.UUID{3},
6566
Ip: pqtype.Inet{
@@ -80,7 +81,11 @@ func TestSlogBackend(t *testing.T) {
8081
)
8182
defer cancel()
8283

83-
err := backend.Export(ctx, alog)
84+
err := backend.Export(ctx, alog, audit.BackendDetails{Actor: &audit.Actor{
85+
ID: uuid.UUID{2},
86+
Username: "coadler",
87+
Email: "doug@coder.com",
88+
}})
8489
require.NoError(t, err)
8590
logger.Sync()
8691

@@ -90,7 +95,7 @@ func TestSlogBackend(t *testing.T) {
9095
err = json.Unmarshal(buf.Bytes(), &s)
9196
require.NoError(t, err)
9297

93-
expected := `{"ID":"01000000-0000-0000-0000-000000000000","Time":"2009-11-10T23:00:00Z","UserID":"02000000-0000-0000-0000-000000000000","OrganizationID":"03000000-0000-0000-0000-000000000000","Ip":"127.0.0.1","UserAgent":"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Safari/537.36","ResourceType":"organization","ResourceID":"04000000-0000-0000-0000-000000000000","ResourceTarget":"colin's organization","Action":"delete","Diff":{"1":2},"StatusCode":204,"AdditionalFields":{"name":"doug","species":"cat"},"RequestID":"05000000-0000-0000-0000-000000000000","ResourceIcon":"photo.png"}`
98+
expected := `{"ID":"01000000-0000-0000-0000-000000000000","Time":"2009-11-10T23:00:00Z","UserID":"02000000-0000-0000-0000-000000000000","OrganizationID":"03000000-0000-0000-0000-000000000000","Ip":"127.0.0.1","UserAgent":"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Safari/537.36","ResourceType":"organization","ResourceID":"04000000-0000-0000-0000-000000000000","ResourceTarget":"colin's organization","Action":"delete","Diff":{"1":2},"StatusCode":204,"AdditionalFields":{"name":"doug","species":"cat"},"RequestID":"05000000-0000-0000-0000-000000000000","ResourceIcon":"photo.png","actor":{"id":"02000000-0000-0000-0000-000000000000","email":"doug@coder.com","username":"coadler"}}`
9499
assert.Equal(t, expected, string(s.Fields))
95100
})
96101
}

enterprise/cli/server.go

+3-1
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,9 @@ func (r *RootCmd) server() *clibase.Cmd {
5050
}
5151
}
5252
options.DERPServer.SetMeshKey(meshKey)
53-
options.Auditor = audit.NewAuditor(audit.DefaultFilter,
53+
options.Auditor = audit.NewAuditor(
54+
options.Database,
55+
audit.DefaultFilter,
5456
backends.NewPostgres(options.Database, true),
5557
backends.NewSlog(options.Logger),
5658
)

enterprise/coderd/coderd_test.go

+5-6
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,16 @@ import (
77
"time"
88

99
"github.com/google/uuid"
10-
11-
"github.com/coder/coder/v2/coderd/database/dbauthz"
12-
"github.com/coder/coder/v2/coderd/rbac"
13-
1410
"github.com/stretchr/testify/assert"
1511
"github.com/stretchr/testify/require"
1612
"go.uber.org/goleak"
1713

1814
agplaudit "github.com/coder/coder/v2/coderd/audit"
1915
"github.com/coder/coder/v2/coderd/coderdtest"
2016
"github.com/coder/coder/v2/coderd/database"
17+
"github.com/coder/coder/v2/coderd/database/dbauthz"
18+
"github.com/coder/coder/v2/coderd/database/dbfake"
19+
"github.com/coder/coder/v2/coderd/rbac"
2120
"github.com/coder/coder/v2/codersdk"
2221
"github.com/coder/coder/v2/enterprise/audit"
2322
"github.com/coder/coder/v2/enterprise/coderd"
@@ -185,7 +184,7 @@ func TestAuditLogging(t *testing.T) {
185184
_, _, api, _ := coderdenttest.NewWithAPI(t, &coderdenttest.Options{
186185
AuditLogging: true,
187186
Options: &coderdtest.Options{
188-
Auditor: audit.NewAuditor(audit.DefaultFilter),
187+
Auditor: audit.NewAuditor(dbfake.New(), audit.DefaultFilter),
189188
},
190189
LicenseOptions: &coderdenttest.LicenseOptions{
191190
Features: license.Features{
@@ -194,7 +193,7 @@ func TestAuditLogging(t *testing.T) {
194193
},
195194
})
196195
auditor := *api.AGPL.Auditor.Load()
197-
ea := audit.NewAuditor(audit.DefaultFilter)
196+
ea := audit.NewAuditor(dbfake.New(), audit.DefaultFilter)
198197
t.Logf("%T = %T", auditor, ea)
199198
assert.EqualValues(t, reflect.ValueOf(ea).Type(), reflect.ValueOf(auditor).Type())
200199
})

0 commit comments

Comments
 (0)