mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat(enterprise/audit): add user object to slog exporter (#9456)
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
|
||||
@@ -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())
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user