feat(enterprise/audit): add user object to slog exporter (#9456)

This commit is contained in:
Colin Adler
2023-08-31 13:32:28 -05:00
committed by GitHub
parent 11d4b6f758
commit 796a9754a9
9 changed files with 71 additions and 24 deletions
+6 -4
View File
@@ -33,12 +33,14 @@ type Request[T Auditable] struct {
Old T
New T
// This optional field can be passed in when the userID cannot be determined from the API Key
// such as in the case of login, when the audit log is created prior the API Key's existence.
// UserID is an optional field can be passed in when the userID cannot be
// determined from the API Key such as in the case of login, when the audit
// log is created prior the API Key's existence.
UserID uuid.UUID
// This optional field can be passed in if the AuditAction must be overridden
// such as in the case of new user authentication when the Audit Action is 'register', not 'login'.
// Action is an optional field can be passed in if the AuditAction must be
// overridden such as in the case of new user authentication when the Audit
// Action is 'register', not 'login'.
Action database.AuditAction
}
+27 -3
View File
@@ -2,23 +2,37 @@ package audit
import (
"context"
"database/sql"
"github.com/google/uuid"
"golang.org/x/xerrors"
"github.com/coder/coder/v2/coderd/audit"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/dbauthz"
)
type BackendDetails struct {
Actor *Actor
}
type Actor struct {
ID uuid.UUID `json:"id"`
Email string `json:"email"`
Username string `json:"username"`
}
// 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
Export(ctx context.Context, alog database.AuditLog, details BackendDetails) error
}
func NewAuditor(filter Filter, backends ...Backend) audit.Auditor {
func NewAuditor(db database.Store, filter Filter, backends ...Backend) audit.Auditor {
return &auditor{
db: db,
filter: filter,
backends: backends,
Differ: audit.Differ{DiffFn: func(old, new any) audit.Map {
@@ -29,6 +43,7 @@ func NewAuditor(filter Filter, backends ...Backend) audit.Auditor {
// auditor is the enterprise implementation of the Auditor interface.
type auditor struct {
db database.Store
filter Filter
backends []Backend
@@ -41,12 +56,21 @@ func (a *auditor) Export(ctx context.Context, alog database.AuditLog) error {
return xerrors.Errorf("filter check: %w", err)
}
actor, err := a.db.GetUserByID(dbauthz.AsSystemRestricted(ctx), alog.UserID) //nolint
if err != nil && !xerrors.Is(err, sql.ErrNoRows) {
return err
}
for _, backend := range a.backends {
if decision&backend.Decision() != backend.Decision() {
continue
}
err = backend.Export(ctx, alog)
err = backend.Export(ctx, alog, BackendDetails{Actor: &Actor{
ID: actor.ID,
Email: actor.Email,
Username: actor.Username,
}})
if err != nil {
// naively return the first error. should probably make this smarter
// by returning multiple errors.
+13 -3
View File
@@ -8,6 +8,7 @@ import (
"golang.org/x/xerrors"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/dbfake"
"github.com/coder/coder/v2/enterprise/audit"
"github.com/coder/coder/v2/enterprise/audit/audittest"
)
@@ -90,6 +91,7 @@ func TestAuditor(t *testing.T) {
var (
backend = &testBackend{decision: test.backendDecision, err: test.backendError}
exporter = audit.NewAuditor(
dbfake.New(),
audit.FilterFunc(func(_ context.Context, _ database.AuditLog) (audit.FilterDecision, error) {
return test.filterDecision, test.filterError
}),
@@ -113,18 +115,26 @@ type testBackend struct {
decision audit.FilterDecision
err error
alogs []database.AuditLog
alogs []testExport
}
type testExport struct {
alog database.AuditLog
details audit.BackendDetails
}
func (t *testBackend) Decision() audit.FilterDecision {
return t.decision
}
func (t *testBackend) Export(_ context.Context, alog database.AuditLog) error {
func (t *testBackend) Export(_ context.Context, alog database.AuditLog, details audit.BackendDetails) error {
if t.err != nil {
return t.err
}
t.alogs = append(t.alogs, alog)
t.alogs = append(t.alogs, testExport{
alog: alog,
details: details,
})
return nil
}
+1 -1
View File
@@ -30,7 +30,7 @@ func (b *postgresBackend) Decision() audit.FilterDecision {
return audit.FilterDecisionExport
}
func (b *postgresBackend) Export(ctx context.Context, alog database.AuditLog) error {
func (b *postgresBackend) Export(ctx context.Context, alog database.AuditLog, _ audit.BackendDetails) error {
_, err := b.db.InsertAuditLog(ctx, database.InsertAuditLogParams(alog))
if err != nil {
return xerrors.Errorf("insert audit log: %w", err)
+2 -1
View File
@@ -8,6 +8,7 @@ import (
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/dbfake"
"github.com/coder/coder/v2/enterprise/audit"
"github.com/coder/coder/v2/enterprise/audit/audittest"
"github.com/coder/coder/v2/enterprise/audit/backends"
)
@@ -25,7 +26,7 @@ func TestPostgresBackend(t *testing.T) {
)
defer cancel()
err := pgb.Export(ctx, alog)
err := pgb.Export(ctx, alog, audit.BackendDetails{})
require.NoError(t, err)
got, err := db.GetAuditLogsOffset(ctx, database.GetAuditLogsOffsetParams{
+5 -1
View File
@@ -24,7 +24,7 @@ func (*slogBackend) Decision() audit.FilterDecision {
return audit.FilterDecisionExport
}
func (b *slogBackend) Export(ctx context.Context, alog database.AuditLog) error {
func (b *slogBackend) Export(ctx context.Context, alog database.AuditLog, details audit.BackendDetails) error {
// We don't use structs.Map because we don't want to recursively convert
// fields into maps. When we keep the type information, slog can more
// 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
fields = append(fields, b.fieldToSlog(sf))
}
if details.Actor != nil {
fields = append(fields, slog.F("actor", details.Actor))
}
b.log.Info(ctx, "audit_log", fields...)
return nil
}
+9 -4
View File
@@ -19,6 +19,7 @@ import (
"cdr.dev/slog"
"cdr.dev/slog/sloggers/slogjson"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/enterprise/audit"
"github.com/coder/coder/v2/enterprise/audit/audittest"
"github.com/coder/coder/v2/enterprise/audit/backends"
)
@@ -39,7 +40,7 @@ func TestSlogBackend(t *testing.T) {
)
defer cancel()
err := backend.Export(ctx, alog)
err := backend.Export(ctx, alog, audit.BackendDetails{})
require.NoError(t, err)
require.Len(t, sink.entries, 1)
require.Equal(t, sink.entries[0].Message, "audit_log")
@@ -59,7 +60,7 @@ func TestSlogBackend(t *testing.T) {
_, inet, _ = net.ParseCIDR("127.0.0.1/32")
alog = database.AuditLog{
ID: uuid.UUID{1},
Time: time.Unix(1257894000, 0),
Time: time.Unix(1257894000, 0).UTC(),
UserID: uuid.UUID{2},
OrganizationID: uuid.UUID{3},
Ip: pqtype.Inet{
@@ -80,7 +81,11 @@ func TestSlogBackend(t *testing.T) {
)
defer cancel()
err := backend.Export(ctx, alog)
err := backend.Export(ctx, alog, audit.BackendDetails{Actor: &audit.Actor{
ID: uuid.UUID{2},
Username: "coadler",
Email: "doug@coder.com",
}})
require.NoError(t, err)
logger.Sync()
@@ -90,7 +95,7 @@ func TestSlogBackend(t *testing.T) {
err = json.Unmarshal(buf.Bytes(), &s)
require.NoError(t, err)
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"}`
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"}}`
assert.Equal(t, expected, string(s.Fields))
})
}
+3 -1
View File
@@ -50,7 +50,9 @@ func (r *RootCmd) server() *clibase.Cmd {
}
}
options.DERPServer.SetMeshKey(meshKey)
options.Auditor = audit.NewAuditor(audit.DefaultFilter,
options.Auditor = audit.NewAuditor(
options.Database,
audit.DefaultFilter,
backends.NewPostgres(options.Database, true),
backends.NewSlog(options.Logger),
)
+5 -6
View File
@@ -7,10 +7,6 @@ import (
"time"
"github.com/google/uuid"
"github.com/coder/coder/v2/coderd/database/dbauthz"
"github.com/coder/coder/v2/coderd/rbac"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/goleak"
@@ -18,6 +14,9 @@ import (
agplaudit "github.com/coder/coder/v2/coderd/audit"
"github.com/coder/coder/v2/coderd/coderdtest"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/dbauthz"
"github.com/coder/coder/v2/coderd/database/dbfake"
"github.com/coder/coder/v2/coderd/rbac"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/enterprise/audit"
"github.com/coder/coder/v2/enterprise/coderd"
@@ -185,7 +184,7 @@ func TestAuditLogging(t *testing.T) {
_, _, api, _ := coderdenttest.NewWithAPI(t, &coderdenttest.Options{
AuditLogging: true,
Options: &coderdtest.Options{
Auditor: audit.NewAuditor(audit.DefaultFilter),
Auditor: audit.NewAuditor(dbfake.New(), audit.DefaultFilter),
},
LicenseOptions: &coderdenttest.LicenseOptions{
Features: license.Features{
@@ -194,7 +193,7 @@ func TestAuditLogging(t *testing.T) {
},
})
auditor := *api.AGPL.Auditor.Load()
ea := audit.NewAuditor(audit.DefaultFilter)
ea := audit.NewAuditor(dbfake.New(), audit.DefaultFilter)
t.Logf("%T = %T", auditor, ea)
assert.EqualValues(t, reflect.ValueOf(ea).Type(), reflect.ValueOf(auditor).Type())
})