Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 0 additions & 55 deletions coderd/audit/exporter.go

This file was deleted.

42 changes: 0 additions & 42 deletions coderd/audit/filter.go

This file was deleted.

65 changes: 57 additions & 8 deletions coderd/audit/request.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,28 @@ package audit

import (
"context"
"encoding/json"
"net"
"net/http"

chimw "github.com/go-chi/chi/v5/middleware"
"github.com/google/uuid"
"github.com/tabbed/pqtype"

"cdr.dev/slog"
"github.com/coder/coder/coderd/database"
"github.com/coder/coder/coderd/httpapi"
)

type RequestParams struct {
Audit Auditor
Log slog.Logger

Action database.AuditAction
ResourceType database.ResourceType
Actor uuid.UUID
Request *http.Request
ResourceID uuid.UUID
ResourceTarget string
Action database.AuditAction
ResourceType database.ResourceType
Actor uuid.UUID
}

type Request[T Auditable] struct {
Expand All @@ -31,9 +37,9 @@ type Request[T Auditable] struct {
// 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)
sw, ok := w.(*httpapi.StatusWriter)
if !ok {
panic("dev error: http.ResponseWriter is not chimw.WrapResponseWriter")
panic("dev error: http.ResponseWriter is not *httpapi.StatusWriter")
}

req := &Request[T]{
Expand All @@ -42,11 +48,54 @@ func InitRequest[T Auditable](w http.ResponseWriter, p *RequestParams) (*Request

return req, func() {
ctx := context.Background()
code := sw.Status()

err := p.Audit.Export(ctx, database.AuditLog{StatusCode: int32(code)})
diff := Diff(p.Audit, req.Old, req.New)
diffRaw, _ := json.Marshal(diff)

ip, err := parseIP(p.Request.RemoteAddr)
if err != nil {
p.Log.Warn(ctx, "parse ip", slog.Error(err))
}

err = p.Audit.Export(ctx, database.AuditLog{
ID: uuid.New(),
Time: database.Now(),
UserID: p.Actor,
Ip: ip,
UserAgent: p.Request.UserAgent(),
ResourceType: p.ResourceType,
ResourceID: p.ResourceID,
ResourceTarget: p.ResourceTarget,
Action: p.Action,
Diff: diffRaw,
StatusCode: int32(sw.Status),
})
if err != nil {
p.Log.Error(ctx, "export audit log", slog.Error(err))
}
}
}

func parseIP(ipStr string) (pqtype.Inet, error) {
var err error

ipStr, _, err = net.SplitHostPort(ipStr)
if err != nil {
return pqtype.Inet{}, err
}

ip := net.ParseIP(ipStr)

ipNet := net.IPNet{}
if ip != nil {
ipNet = net.IPNet{
IP: ip,
Mask: net.CIDRMask(len(ip)*8, len(ip)*8),
}
}

return pqtype.Inet{
IPNet: ipNet,
Valid: ip != nil,
}, nil
}
8 changes: 7 additions & 1 deletion coderd/database/time.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,11 @@ import "time"

// Now returns a standardized timezone used for database resources.
func Now() time.Time {
return time.Now().UTC()
return Time(time.Now().UTC())
}

// Time returns a time compatible with Postgres. Postgres only stores dates with
// microsecond precision.
func Time(t time.Time) time.Time {
return t.Round(time.Microsecond)
}
33 changes: 26 additions & 7 deletions enterprise/audit/audit.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package audit
import (
"context"

"golang.org/x/xerrors"

"github.com/coder/coder/coderd/audit"
"github.com/coder/coder/coderd/database"
)
Expand All @@ -15,8 +17,10 @@ type Backend interface {
Export(ctx context.Context, alog database.AuditLog) error
}

func NewAuditor() audit.Auditor {
func NewAuditor(filter Filter, backends ...Backend) audit.Auditor {
return &auditor{
filter: filter,
backends: backends,
Differ: audit.Differ{DiffFn: func(old, new any) audit.Map {
return diffValues(old, new, AuditableResources)
}},
Expand All @@ -25,15 +29,30 @@ func NewAuditor() audit.Auditor {

// auditor is the enterprise implementation of the Auditor interface.
type auditor struct {
//nolint:unused
filter Filter
//nolint:unused
filter Filter
backends []Backend

audit.Differ
}

//nolint:unused
func (*auditor) Export(context.Context, database.AuditLog) error {
panic("not implemented") // TODO: Implement
func (a *auditor) Export(ctx context.Context, alog database.AuditLog) error {
decision, err := a.filter.Check(ctx, alog)
if err != nil {
return xerrors.Errorf("filter check: %w", err)
}

for _, backend := range a.backends {
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
}
67 changes: 33 additions & 34 deletions coderd/audit/exporter_test.go → enterprise/audit/audit_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,25 @@ package audit_test

import (
"context"
"net"
"net/http"
"testing"
"time"

"github.com/google/uuid"
"github.com/stretchr/testify/require"
"github.com/tabbed/pqtype"
"golang.org/x/xerrors"

"github.com/coder/coder/coderd/audit"
"github.com/coder/coder/coderd/database"
"github.com/coder/coder/enterprise/audit"
"github.com/coder/coder/enterprise/audit/audittest"
)

func TestExporter(t *testing.T) {
func TestAuditor(t *testing.T) {
t.Parallel()

var tests = []struct {
name string
filterDecision audit.FilterDecision
filterError error
backendDecision audit.FilterDecision
backendError error
shouldExport bool
}{
{
Expand Down Expand Up @@ -60,11 +59,22 @@ func TestExporter(t *testing.T) {
backendDecision: audit.FilterDecisionExport,
shouldExport: true,
},
{
name: "FilterError",
filterError: xerrors.New("filter errored"),
backendDecision: audit.FilterDecisionExport,
shouldExport: false,
},
{
name: "BackendError",
backendError: xerrors.New("backend errored"),
shouldExport: false,
},
// When more filters are written they should have their own tests.
{
name: "DefaultFilter",
filterDecision: func() audit.FilterDecision {
decision, _ := audit.DefaultFilter.Check(context.Background(), randomAuditLog())
decision, _ := audit.DefaultFilter.Check(context.Background(), audittest.RandomLog())
return decision
}(),
backendDecision: audit.FilterDecisionExport,
Expand All @@ -78,45 +88,30 @@ func TestExporter(t *testing.T) {
t.Parallel()

var (
backend = &testBackend{decision: test.backendDecision}
exporter = audit.NewExporter(
backend = &testBackend{decision: test.backendDecision, err: test.backendError}
exporter = audit.NewAuditor(
audit.FilterFunc(func(_ context.Context, _ database.AuditLog) (audit.FilterDecision, error) {
return test.filterDecision, nil
return test.filterDecision, test.filterError
}),
backend,
)
)

err := exporter.Export(context.Background(), randomAuditLog())
require.NoError(t, err)
err := exporter.Export(context.Background(), audittest.RandomLog())
if test.filterError != nil {
require.ErrorIs(t, err, test.filterError)
} else if test.backendError != nil {
require.ErrorIs(t, err, test.backendError)
}

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
err error

alogs []database.AuditLog
}
Expand All @@ -126,6 +121,10 @@ func (t *testBackend) Decision() audit.FilterDecision {
}

func (t *testBackend) Export(_ context.Context, alog database.AuditLog) error {
if t.err != nil {
return t.err
}

t.alogs = append(t.alogs, alog)
return nil
}
Loading