Skip to content

Commit 92550b6

Browse files
committed
feat: audit log api
1 parent 5301d36 commit 92550b6

File tree

13 files changed

+416
-59
lines changed

13 files changed

+416
-59
lines changed

cli/portforward.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ import (
1717
"golang.org/x/xerrors"
1818

1919
"cdr.dev/slog"
20-
2120
"github.com/coder/coder/agent"
2221
"github.com/coder/coder/cli/cliui"
2322
"github.com/coder/coder/codersdk"

cli/speedtest.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,15 @@ import (
55
"fmt"
66
"time"
77

8-
"cdr.dev/slog"
9-
"github.com/coder/coder/cli/cliflag"
10-
"github.com/coder/coder/cli/cliui"
11-
"github.com/coder/coder/codersdk"
128
"github.com/jedib0t/go-pretty/v6/table"
139
"github.com/spf13/cobra"
1410
"golang.org/x/xerrors"
1511
tsspeedtest "tailscale.com/net/speedtest"
12+
13+
"cdr.dev/slog"
14+
"github.com/coder/coder/cli/cliflag"
15+
"github.com/coder/coder/cli/cliui"
16+
"github.com/coder/coder/codersdk"
1617
)
1718

1819
func speedtest() *cobra.Command {

cli/speedtest_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,15 @@ import (
44
"context"
55
"testing"
66

7+
"github.com/stretchr/testify/assert"
8+
79
"cdr.dev/slog/sloggers/slogtest"
810
"github.com/coder/coder/agent"
911
"github.com/coder/coder/cli/clitest"
1012
"github.com/coder/coder/coderd/coderdtest"
1113
"github.com/coder/coder/codersdk"
1214
"github.com/coder/coder/pty/ptytest"
1315
"github.com/coder/coder/testutil"
14-
"github.com/stretchr/testify/assert"
1516
)
1617

1718
func TestSpeedtest(t *testing.T) {

coderd/audit.go

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
package coderd
2+
3+
import (
4+
"encoding/json"
5+
"net"
6+
"net/http"
7+
"net/netip"
8+
"time"
9+
10+
"github.com/google/uuid"
11+
"github.com/tabbed/pqtype"
12+
13+
"github.com/coder/coder/coderd/database"
14+
"github.com/coder/coder/coderd/httpapi"
15+
"github.com/coder/coder/coderd/httpmw"
16+
"github.com/coder/coder/coderd/rbac"
17+
"github.com/coder/coder/codersdk"
18+
)
19+
20+
func (api *API) auditLogs(rw http.ResponseWriter, r *http.Request) {
21+
if !api.Authorize(r, rbac.ActionRead, rbac.ResourceAuditLog) {
22+
httpapi.Forbidden(rw)
23+
return
24+
}
25+
26+
ctx := r.Context()
27+
page, ok := parsePagination(rw, r)
28+
if !ok {
29+
return
30+
}
31+
32+
dblogs, err := api.Database.GetAuditLogsOffset(ctx, database.GetAuditLogsOffsetParams{
33+
Offset: int32(page.Offset),
34+
Limit: int32(page.Limit),
35+
})
36+
if err != nil {
37+
httpapi.InternalServerError(rw, err)
38+
return
39+
}
40+
41+
httpapi.Write(rw, http.StatusOK, codersdk.AuditLogResponse{
42+
AuditLogs: convertAuditLogs(dblogs),
43+
})
44+
}
45+
46+
func (api *API) auditLogCount(rw http.ResponseWriter, r *http.Request) {
47+
ctx := r.Context()
48+
if !api.Authorize(r, rbac.ActionRead, rbac.ResourceAuditLog) {
49+
httpapi.Forbidden(rw)
50+
return
51+
}
52+
53+
count, err := api.Database.GetAuditLogCount(ctx)
54+
if err != nil {
55+
httpapi.InternalServerError(rw, err)
56+
return
57+
}
58+
59+
httpapi.Write(rw, http.StatusOK, codersdk.AuditLogCountResponse{
60+
Count: count,
61+
})
62+
}
63+
64+
func (api *API) generateFakeAuditLog(rw http.ResponseWriter, r *http.Request) {
65+
ctx := r.Context()
66+
if !api.Authorize(r, rbac.ActionRead, rbac.ResourceAuditLog) {
67+
httpapi.Forbidden(rw)
68+
return
69+
}
70+
71+
key := httpmw.APIKey(r)
72+
user, err := api.Database.GetUserByID(ctx, key.UserID)
73+
if err != nil {
74+
httpapi.InternalServerError(rw, err)
75+
return
76+
}
77+
78+
diff, err := json.Marshal(codersdk.AuditDiff{
79+
"foo": codersdk.AuditDiffField{Old: "bar", New: "baz"},
80+
})
81+
if err != nil {
82+
httpapi.InternalServerError(rw, err)
83+
return
84+
}
85+
86+
ipRaw, _, _ := net.SplitHostPort(r.RemoteAddr)
87+
ip := net.ParseIP(ipRaw)
88+
ipNet := pqtype.Inet{}
89+
if ip != nil {
90+
ipNet = pqtype.Inet{
91+
IPNet: net.IPNet{
92+
IP: ip,
93+
Mask: net.CIDRMask(len(ip)*8, len(ip)*8),
94+
},
95+
Valid: true,
96+
}
97+
}
98+
99+
_, err = api.Database.InsertAuditLog(ctx, database.InsertAuditLogParams{
100+
ID: uuid.New(),
101+
Time: time.Now(),
102+
UserID: user.ID,
103+
Ip: ipNet,
104+
UserAgent: r.UserAgent(),
105+
ResourceType: database.ResourceTypeUser,
106+
ResourceID: user.ID,
107+
ResourceTarget: user.Username,
108+
Action: database.AuditActionWrite,
109+
Diff: diff,
110+
StatusCode: http.StatusOK,
111+
})
112+
if err != nil {
113+
httpapi.InternalServerError(rw, err)
114+
return
115+
}
116+
117+
rw.WriteHeader(http.StatusNoContent)
118+
}
119+
120+
func convertAuditLogs(dblogs []database.GetAuditLogsOffsetRow) []codersdk.AuditLog {
121+
alogs := make([]codersdk.AuditLog, 0, len(dblogs))
122+
123+
for _, dblog := range dblogs {
124+
alogs = append(alogs, convertAuditLog(dblog))
125+
}
126+
127+
return alogs
128+
}
129+
130+
func convertAuditLog(dblog database.GetAuditLogsOffsetRow) codersdk.AuditLog {
131+
ip, _ := netip.AddrFromSlice(dblog.Ip.IPNet.IP)
132+
133+
diff := codersdk.AuditDiff{}
134+
_ = json.Unmarshal(dblog.Diff, &diff)
135+
136+
var user *codersdk.User
137+
if dblog.UserUsername.Valid {
138+
user = &codersdk.User{
139+
ID: dblog.UserID,
140+
Username: dblog.UserUsername.String,
141+
Email: dblog.UserEmail.String,
142+
CreatedAt: dblog.UserCreatedAt.Time,
143+
Status: codersdk.UserStatus(dblog.UserStatus),
144+
Roles: []codersdk.Role{},
145+
}
146+
147+
for _, roleName := range dblog.UserRoles {
148+
rbacRole, _ := rbac.RoleByName(roleName)
149+
user.Roles = append(user.Roles, convertRole(rbacRole))
150+
}
151+
}
152+
153+
return codersdk.AuditLog{
154+
ID: dblog.ID,
155+
RequestID: dblog.RequestID,
156+
Time: dblog.Time,
157+
OrganizationID: dblog.OrganizationID,
158+
IP: ip,
159+
UserAgent: dblog.UserAgent,
160+
ResourceType: codersdk.ResourceType(dblog.ResourceType),
161+
ResourceID: dblog.ResourceID,
162+
ResourceTarget: dblog.ResourceTarget,
163+
ResourceIcon: dblog.ResourceIcon,
164+
Action: codersdk.AuditAction(dblog.Action),
165+
Diff: diff,
166+
StatusCode: dblog.StatusCode,
167+
AdditionalFields: dblog.AdditionalFields,
168+
Description: "",
169+
User: user,
170+
}
171+
}

coderd/audit_test.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package coderd_test
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"github.com/stretchr/testify/require"
8+
9+
"github.com/coder/coder/coderd/coderdtest"
10+
"github.com/coder/coder/codersdk"
11+
)
12+
13+
func TestAuditLogs(t *testing.T) {
14+
t.Parallel()
15+
16+
t.Run("OK", func(t *testing.T) {
17+
t.Parallel()
18+
19+
ctx := context.Background()
20+
client := coderdtest.New(t, nil)
21+
_ = coderdtest.CreateFirstUser(t, client)
22+
23+
err := client.CreateTestAuditLog(ctx)
24+
require.NoError(t, err)
25+
26+
count, err := client.AuditLogCount(ctx)
27+
require.NoError(t, err)
28+
29+
alogs, err := client.AuditLogs(ctx, codersdk.Pagination{Limit: 1})
30+
require.NoError(t, err)
31+
32+
require.Equal(t, int64(1), count.Count)
33+
require.Len(t, alogs.AuditLogs, 1)
34+
})
35+
}

coderd/coderd.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,15 @@ func New(options *Options) *API {
220220
})
221221
})
222222
})
223+
r.Route("/audit", func(r chi.Router) {
224+
r.Use(
225+
apiKeyMiddleware,
226+
)
227+
228+
r.Get("/", api.auditLogs)
229+
r.Get("/count", api.auditLogCount)
230+
r.Post("/testgenerate", api.generateFakeAuditLog)
231+
})
223232
r.Route("/files", func(r chi.Router) {
224233
r.Use(
225234
apiKeyMiddleware,

coderd/database/databasefake/databasefake.go

Lines changed: 39 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2303,43 +2303,59 @@ func (q *fakeQuerier) DeleteGitSSHKey(_ context.Context, userID uuid.UUID) error
23032303
return sql.ErrNoRows
23042304
}
23052305

2306-
func (q *fakeQuerier) GetAuditLogsBefore(_ context.Context, arg database.GetAuditLogsBeforeParams) ([]database.AuditLog, error) {
2306+
func (q *fakeQuerier) GetAuditLogsOffset(ctx context.Context, arg database.GetAuditLogsOffsetParams) ([]database.GetAuditLogsOffsetRow, error) {
23072307
q.mutex.RLock()
23082308
defer q.mutex.RUnlock()
23092309

2310-
logs := make([]database.AuditLog, 0)
2311-
start := database.AuditLog{}
2312-
2313-
if arg.ID != uuid.Nil {
2314-
for _, alog := range q.auditLogs {
2315-
if alog.ID == arg.ID {
2316-
start = alog
2317-
break
2318-
}
2319-
}
2320-
} else {
2321-
start.ID = uuid.New()
2322-
start.Time = arg.StartTime
2323-
}
2324-
2325-
if start.ID == uuid.Nil {
2326-
return nil, sql.ErrNoRows
2327-
}
2310+
logs := make([]database.GetAuditLogsOffsetRow, 0, arg.Limit)
23282311

23292312
// q.auditLogs are already sorted by time DESC, so no need to sort after the fact.
23302313
for _, alog := range q.auditLogs {
2331-
if alog.Time.Before(start.Time) {
2332-
logs = append(logs, alog)
2333-
}
2314+
if arg.Offset > 0 {
2315+
arg.Offset--
2316+
continue
2317+
}
2318+
2319+
user, err := q.GetUserByID(ctx, alog.UserID)
2320+
userValid := err == nil
2321+
2322+
logs = append(logs, database.GetAuditLogsOffsetRow{
2323+
ID: alog.ID,
2324+
RequestID: alog.RequestID,
2325+
OrganizationID: alog.OrganizationID,
2326+
Ip: alog.Ip,
2327+
UserAgent: alog.UserAgent,
2328+
ResourceType: database.ResourceType(alog.UserAgent),
2329+
ResourceID: alog.ResourceID,
2330+
ResourceTarget: alog.ResourceTarget,
2331+
ResourceIcon: alog.ResourceIcon,
2332+
Action: alog.Action,
2333+
Diff: alog.Diff,
2334+
StatusCode: alog.StatusCode,
2335+
AdditionalFields: alog.AdditionalFields,
2336+
UserID: alog.UserID,
2337+
UserUsername: sql.NullString{String: user.Username, Valid: userValid},
2338+
UserEmail: sql.NullString{String: user.Email, Valid: userValid},
2339+
UserCreatedAt: sql.NullTime{Time: user.CreatedAt, Valid: userValid},
2340+
UserStatus: user.Status,
2341+
UserRoles: user.RBACRoles,
2342+
})
23342343

2335-
if len(logs) >= int(arg.RowLimit) {
2344+
if len(logs) >= int(arg.Limit) {
23362345
break
23372346
}
23382347
}
23392348

23402349
return logs, nil
23412350
}
23422351

2352+
func (q *fakeQuerier) GetAuditLogCount(_ context.Context) (int64, error) {
2353+
q.mutex.RLock()
2354+
defer q.mutex.RUnlock()
2355+
2356+
return int64(len(q.auditLogs)), nil
2357+
}
2358+
23432359
func (q *fakeQuerier) InsertAuditLog(_ context.Context, arg database.InsertAuditLogParams) (database.AuditLog, error) {
23442360
q.mutex.Lock()
23452361
defer q.mutex.Unlock()

coderd/database/querier.go

Lines changed: 3 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)