Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit20caee1

Browse files
authored
feat: add audit exporting and filtering (#1314)
1 parentac27f64 commit20caee1

File tree

15 files changed

+448
-14
lines changed

15 files changed

+448
-14
lines changed

‎coderd/audit/backends/postgres.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package backends
2+
3+
import (
4+
"context"
5+
6+
"golang.org/x/xerrors"
7+
8+
"github.com/coder/coder/coderd/audit"
9+
"github.com/coder/coder/coderd/database"
10+
)
11+
12+
typepostgresBackendstruct {
13+
// internal indicates if the exporter is exporting to the Postgres database
14+
// that the rest of Coderd uses. Since this is a generic Postgres exporter,
15+
// we make different decisions to store the audit log based on if it's
16+
// pointing to the Coderd database.
17+
internalbool
18+
db database.Store
19+
}
20+
21+
funcNewPostgres(db database.Store,internalbool) audit.Backend {
22+
return&postgresBackend{db:db,internal:internal}
23+
}
24+
25+
func (b*postgresBackend)Decision() audit.FilterDecision {
26+
ifb.internal {
27+
returnaudit.FilterDecisionStore
28+
}
29+
30+
returnaudit.FilterDecisionExport
31+
}
32+
33+
func (b*postgresBackend)Export(ctx context.Context,alog database.AuditLog)error {
34+
_,err:=b.db.InsertAuditLog(ctx, database.InsertAuditLogParams{
35+
ID:alog.ID,
36+
Time:alog.Time,
37+
UserID:alog.UserID,
38+
OrganizationID:alog.OrganizationID,
39+
Ip:alog.Ip,
40+
UserAgent:alog.UserAgent,
41+
ResourceType:alog.ResourceType,
42+
ResourceID:alog.ResourceID,
43+
ResourceTarget:alog.ResourceTarget,
44+
Action:alog.Action,
45+
Diff:alog.Diff,
46+
StatusCode:alog.StatusCode,
47+
})
48+
iferr!=nil {
49+
returnxerrors.Errorf("insert audit log: %w",err)
50+
}
51+
52+
returnnil
53+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package backends_test
2+
3+
import (
4+
"context"
5+
"net"
6+
"net/http"
7+
"testing"
8+
"time"
9+
10+
"github.com/google/uuid"
11+
"github.com/stretchr/testify/require"
12+
"github.com/tabbed/pqtype"
13+
14+
"github.com/coder/coder/coderd/audit/backends"
15+
"github.com/coder/coder/coderd/database"
16+
"github.com/coder/coder/coderd/database/databasefake"
17+
)
18+
19+
funcTestPostgresBackend(t*testing.T) {
20+
t.Parallel()
21+
t.Run("OK",func(t*testing.T) {
22+
t.Parallel()
23+
24+
var (
25+
ctx,cancel=context.WithCancel(context.Background())
26+
db=databasefake.New()
27+
pgb=backends.NewPostgres(db,true)
28+
alog=randomAuditLog()
29+
)
30+
defercancel()
31+
32+
err:=pgb.Export(ctx,alog)
33+
require.NoError(t,err)
34+
35+
got,err:=db.GetAuditLogsBefore(ctx, database.GetAuditLogsBeforeParams{
36+
ID:uuid.Nil,
37+
StartTime:time.Now().Add(time.Second),
38+
RowLimit:1,
39+
})
40+
require.NoError(t,err)
41+
require.Len(t,got,1)
42+
require.Equal(t,alog,got[0])
43+
})
44+
}
45+
46+
funcrandomAuditLog() database.AuditLog {
47+
_,inet,_:=net.ParseCIDR("127.0.0.1/32")
48+
return database.AuditLog{
49+
ID:uuid.New(),
50+
Time:time.Now(),
51+
UserID:uuid.New(),
52+
OrganizationID:uuid.New(),
53+
Ip: pqtype.Inet{
54+
IPNet:*inet,
55+
Valid:true,
56+
},
57+
UserAgent:"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Safari/537.36",
58+
ResourceType:database.ResourceTypeOrganization,
59+
ResourceID:uuid.New(),
60+
ResourceTarget:"colin's organization",
61+
Action:database.AuditActionDelete,
62+
Diff: []byte{},
63+
StatusCode:http.StatusNoContent,
64+
}
65+
}

‎coderd/audit/backends/slog.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package backends
2+
3+
import (
4+
"context"
5+
6+
"github.com/fatih/structs"
7+
8+
"cdr.dev/slog"
9+
"github.com/coder/coder/coderd/audit"
10+
"github.com/coder/coder/coderd/database"
11+
)
12+
13+
typeslogBackendstruct {
14+
log slog.Logger
15+
}
16+
17+
funcNewSlog(logger slog.Logger) audit.Backend {
18+
returnslogBackend{log:logger}
19+
}
20+
21+
func (slogBackend)Decision() audit.FilterDecision {
22+
returnaudit.FilterDecisionExport
23+
}
24+
25+
func (bslogBackend)Export(ctx context.Context,alog database.AuditLog)error {
26+
m:=structs.Map(alog)
27+
fields:=make([]slog.Field,0,len(m))
28+
fork,v:=rangem {
29+
fields=append(fields,slog.F(k,v))
30+
}
31+
32+
b.log.Info(ctx,"audit_log",fields...)
33+
returnnil
34+
}

‎coderd/audit/backends/slog_test.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package backends_test
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"github.com/fatih/structs"
8+
"github.com/stretchr/testify/require"
9+
10+
"cdr.dev/slog"
11+
"github.com/coder/coder/coderd/audit/backends"
12+
)
13+
14+
funcTestSlogBackend(t*testing.T) {
15+
t.Parallel()
16+
t.Run("OK",func(t*testing.T) {
17+
t.Parallel()
18+
19+
var (
20+
ctx,cancel=context.WithCancel(context.Background())
21+
22+
sink=&fakeSink{}
23+
logger=slog.Make(sink)
24+
backend=backends.NewSlog(logger)
25+
26+
alog=randomAuditLog()
27+
)
28+
defercancel()
29+
30+
err:=backend.Export(ctx,alog)
31+
require.NoError(t,err)
32+
require.Len(t,sink.entries,1)
33+
require.Equal(t,sink.entries[0].Message,"audit_log")
34+
require.Len(t,sink.entries[0].Fields,len(structs.Fields(alog)))
35+
})
36+
}
37+
38+
typefakeSinkstruct {
39+
entries []slog.SinkEntry
40+
}
41+
42+
func (s*fakeSink)LogEntry(_ context.Context,e slog.SinkEntry) {
43+
s.entries=append(s.entries,e)
44+
}
45+
46+
func (*fakeSink)Sync() {}

‎coderd/audit/exporter.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package audit
2+
3+
import (
4+
"context"
5+
6+
"golang.org/x/xerrors"
7+
8+
"github.com/coder/coder/coderd/database"
9+
)
10+
11+
// Backends can store or send audit logs to arbitrary locations.
12+
typeBackendinterface {
13+
// Decision determines the FilterDecisions that the backend tolerates.
14+
Decision()FilterDecision
15+
// Export sends an audit log to the backend.
16+
Export(ctx context.Context,alog database.AuditLog)error
17+
}
18+
19+
// Exporter exports audit logs to an arbitrary list of backends.
20+
typeExporterstruct {
21+
filterFilter
22+
backends []Backend
23+
}
24+
25+
// NewExporter creates an exporter from the given filter and backends.
26+
funcNewExporter(filterFilter,backends...Backend)*Exporter {
27+
return&Exporter{
28+
filter:filter,
29+
backends:backends,
30+
}
31+
}
32+
33+
// Export exports and audit log. Before exporting to a backend, it uses the
34+
// filter to determine if the backend tolerates the audit log. If not, it is
35+
// dropped.
36+
func (e*Exporter)Export(ctx context.Context,alog database.AuditLog)error {
37+
decision,err:=e.filter.Check(ctx,alog)
38+
iferr!=nil {
39+
returnxerrors.Errorf("filter check: %w",err)
40+
}
41+
42+
for_,backend:=rangee.backends {
43+
ifdecision&backend.Decision()!=backend.Decision() {
44+
continue
45+
}
46+
47+
err=backend.Export(ctx,alog)
48+
iferr!=nil {
49+
// naively return the first error. should probably make this smarter
50+
// by returning multiple errors.
51+
returnxerrors.Errorf("export audit log to backend: %w",err)
52+
}
53+
}
54+
returnnil
55+
}

‎coderd/audit/exporter_test.go

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
package audit_test
2+
3+
import (
4+
"context"
5+
"net"
6+
"net/http"
7+
"testing"
8+
"time"
9+
10+
"github.com/google/uuid"
11+
"github.com/stretchr/testify/require"
12+
"github.com/tabbed/pqtype"
13+
14+
"github.com/coder/coder/coderd/audit"
15+
"github.com/coder/coder/coderd/database"
16+
)
17+
18+
funcTestExporter(t*testing.T) {
19+
t.Parallel()
20+
21+
vartests= []struct {
22+
namestring
23+
filterDecision audit.FilterDecision
24+
backendDecision audit.FilterDecision
25+
shouldExportbool
26+
}{
27+
{
28+
name:"ShouldDrop",
29+
filterDecision:audit.FilterDecisionDrop,
30+
backendDecision:audit.FilterDecisionStore,
31+
shouldExport:false,
32+
},
33+
{
34+
name:"ShouldStore",
35+
filterDecision:audit.FilterDecisionStore,
36+
backendDecision:audit.FilterDecisionStore,
37+
shouldExport:true,
38+
},
39+
{
40+
name:"ShouldNotStore",
41+
filterDecision:audit.FilterDecisionExport,
42+
backendDecision:audit.FilterDecisionStore,
43+
shouldExport:false,
44+
},
45+
{
46+
name:"ShouldExport",
47+
filterDecision:audit.FilterDecisionExport,
48+
backendDecision:audit.FilterDecisionExport,
49+
shouldExport:true,
50+
},
51+
{
52+
name:"ShouldNotExport",
53+
filterDecision:audit.FilterDecisionStore,
54+
backendDecision:audit.FilterDecisionExport,
55+
shouldExport:false,
56+
},
57+
{
58+
name:"ShouldStoreOrExport",
59+
filterDecision:audit.FilterDecisionStore|audit.FilterDecisionExport,
60+
backendDecision:audit.FilterDecisionExport,
61+
shouldExport:true,
62+
},
63+
// When more filters are written they should have their own tests.
64+
{
65+
name:"DefaultFilter",
66+
filterDecision:func() audit.FilterDecision {
67+
decision,_:=audit.DefaultFilter.Check(context.Background(),randomAuditLog())
68+
returndecision
69+
}(),
70+
backendDecision:audit.FilterDecisionExport,
71+
shouldExport:true,
72+
},
73+
}
74+
75+
for_,test:=rangetests {
76+
test:=test
77+
t.Run(test.name,func(t*testing.T) {
78+
t.Parallel()
79+
80+
var (
81+
backend=&testBackend{decision:test.backendDecision}
82+
exporter=audit.NewExporter(
83+
audit.FilterFunc(func(_ context.Context,_ database.AuditLog) (audit.FilterDecision,error) {
84+
returntest.filterDecision,nil
85+
}),
86+
backend,
87+
)
88+
)
89+
90+
err:=exporter.Export(context.Background(),randomAuditLog())
91+
require.NoError(t,err)
92+
require.Equal(t,len(backend.alogs)>0,test.shouldExport)
93+
})
94+
}
95+
}
96+
97+
funcrandomAuditLog() database.AuditLog {
98+
_,inet,_:=net.ParseCIDR("127.0.0.1/32")
99+
return database.AuditLog{
100+
ID:uuid.New(),
101+
Time:time.Now(),
102+
UserID:uuid.New(),
103+
OrganizationID:uuid.New(),
104+
Ip: pqtype.Inet{
105+
IPNet:*inet,
106+
Valid:true,
107+
},
108+
UserAgent:"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Safari/537.36",
109+
ResourceType:database.ResourceTypeOrganization,
110+
ResourceID:uuid.New(),
111+
ResourceTarget:"colin's organization",
112+
Action:database.AuditActionDelete,
113+
Diff: []byte{},
114+
StatusCode:http.StatusNoContent,
115+
}
116+
}
117+
118+
typetestBackendstruct {
119+
decision audit.FilterDecision
120+
121+
alogs []database.AuditLog
122+
}
123+
124+
func (t*testBackend)Decision() audit.FilterDecision {
125+
returnt.decision
126+
}
127+
128+
func (t*testBackend)Export(_ context.Context,alog database.AuditLog)error {
129+
t.alogs=append(t.alogs,alog)
130+
returnnil
131+
}

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp