Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit bf8d823

Browse files
feat: Add audit log filters in the API (#4078)
1 parent f314f30 commit bf8d823

File tree

8 files changed

+238
-20
lines changed

8 files changed

+238
-20
lines changed

coderd/audit.go

Lines changed: 68 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import (
66
"net"
77
"net/http"
88
"net/netip"
9+
"net/url"
10+
"strings"
911
"time"
1012

1113
"github.com/google/uuid"
@@ -30,9 +32,21 @@ func (api *API) auditLogs(rw http.ResponseWriter, r *http.Request) {
3032
return
3133
}
3234

35+
queryStr := r.URL.Query().Get("q")
36+
filter, errs := auditSearchQuery(queryStr)
37+
if len(errs) > 0 {
38+
httpapi.Write(rw, http.StatusBadRequest, codersdk.Response{
39+
Message: "Invalid audit search query.",
40+
Validations: errs,
41+
})
42+
return
43+
}
44+
3345
dblogs, err := api.Database.GetAuditLogsOffset(ctx, database.GetAuditLogsOffsetParams{
34-
Offset: int32(page.Offset),
35-
Limit: int32(page.Limit),
46+
Offset: int32(page.Offset),
47+
Limit: int32(page.Limit),
48+
ResourceType: filter.ResourceType,
49+
Action: filter.Action,
3650
})
3751
if err != nil {
3852
httpapi.InternalServerError(rw, err)
@@ -97,16 +111,27 @@ func (api *API) generateFakeAuditLog(rw http.ResponseWriter, r *http.Request) {
97111
}
98112
}
99113

114+
var params codersdk.CreateTestAuditLogRequest
115+
if !httpapi.Read(rw, r, &params) {
116+
return
117+
}
118+
if params.Action == "" {
119+
params.Action = codersdk.AuditActionWrite
120+
}
121+
if params.ResourceType == "" {
122+
params.ResourceType = codersdk.ResourceTypeUser
123+
}
124+
100125
_, err = api.Database.InsertAuditLog(ctx, database.InsertAuditLogParams{
101126
ID: uuid.New(),
102127
Time: time.Now(),
103128
UserID: user.ID,
104129
Ip: ipNet,
105130
UserAgent: r.UserAgent(),
106-
ResourceType: database.ResourceTypeUser,
131+
ResourceType: database.ResourceType(params.ResourceType),
107132
ResourceID: user.ID,
108133
ResourceTarget: user.Username,
109-
Action: database.AuditActionWrite,
134+
Action: database.AuditAction(params.Action),
110135
Diff: diff,
111136
StatusCode: http.StatusOK,
112137
AdditionalFields: []byte("{}"),
@@ -179,3 +204,42 @@ func auditLogDescription(alog database.GetAuditLogsOffsetRow) string {
179204
codersdk.ResourceType(alog.ResourceType).FriendlyString(),
180205
)
181206
}
207+
208+
// auditSearchQuery takes a query string and returns the auditLog filter.
209+
// It also can return the list of validation errors to return to the api.
210+
func auditSearchQuery(query string) (database.GetAuditLogsOffsetParams, []codersdk.ValidationError) {
211+
searchParams := make(url.Values)
212+
if query == "" {
213+
// No filter
214+
return database.GetAuditLogsOffsetParams{}, nil
215+
}
216+
query = strings.ToLower(query)
217+
// Because we do this in 2 passes, we want to maintain quotes on the first
218+
// pass.Further splitting occurs on the second pass and quotes will be
219+
// dropped.
220+
elements := splitQueryParameterByDelimiter(query, ' ', true)
221+
for _, element := range elements {
222+
parts := splitQueryParameterByDelimiter(element, ':', false)
223+
switch len(parts) {
224+
case 1:
225+
// No key:value pair.
226+
searchParams.Set("resource_type", parts[0])
227+
case 2:
228+
searchParams.Set(parts[0], parts[1])
229+
default:
230+
return database.GetAuditLogsOffsetParams{}, []codersdk.ValidationError{
231+
{Field: "q", Detail: fmt.Sprintf("Query element %q can only contain 1 ':'", element)},
232+
}
233+
}
234+
}
235+
236+
// Using the query param parser here just returns consistent errors with
237+
// other parsing.
238+
parser := httpapi.NewQueryParamParser()
239+
filter := database.GetAuditLogsOffsetParams{
240+
ResourceType: parser.String(searchParams, "", "resource_type"),
241+
Action: parser.String(searchParams, "", "action"),
242+
}
243+
244+
return filter, parser.Errors
245+
}

coderd/audit_test.go

Lines changed: 79 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,16 +20,93 @@ func TestAuditLogs(t *testing.T) {
2020
client := coderdtest.New(t, nil)
2121
_ = coderdtest.CreateFirstUser(t, client)
2222

23-
err := client.CreateTestAuditLog(ctx)
23+
err := client.CreateTestAuditLog(ctx, codersdk.CreateTestAuditLogRequest{})
2424
require.NoError(t, err)
2525

2626
count, err := client.AuditLogCount(ctx)
2727
require.NoError(t, err)
2828

29-
alogs, err := client.AuditLogs(ctx, codersdk.Pagination{Limit: 1})
29+
alogs, err := client.AuditLogs(ctx, codersdk.AuditLogsRequest{
30+
Pagination: codersdk.Pagination{
31+
Limit: 1,
32+
},
33+
})
3034
require.NoError(t, err)
3135

3236
require.Equal(t, int64(1), count.Count)
3337
require.Len(t, alogs.AuditLogs, 1)
3438
})
3539
}
40+
41+
func TestAuditLogsFilter(t *testing.T) {
42+
t.Parallel()
43+
44+
t.Run("FilterByResourceType", func(t *testing.T) {
45+
t.Parallel()
46+
47+
ctx := context.Background()
48+
client := coderdtest.New(t, nil)
49+
_ = coderdtest.CreateFirstUser(t, client)
50+
51+
// Create two logs with "Create"
52+
err := client.CreateTestAuditLog(ctx, codersdk.CreateTestAuditLogRequest{
53+
Action: codersdk.AuditActionCreate,
54+
ResourceType: codersdk.ResourceTypeTemplate,
55+
})
56+
require.NoError(t, err)
57+
err = client.CreateTestAuditLog(ctx, codersdk.CreateTestAuditLogRequest{
58+
Action: codersdk.AuditActionCreate,
59+
ResourceType: codersdk.ResourceTypeUser,
60+
})
61+
require.NoError(t, err)
62+
63+
// Create one log with "Delete"
64+
err = client.CreateTestAuditLog(ctx, codersdk.CreateTestAuditLogRequest{
65+
Action: codersdk.AuditActionDelete,
66+
ResourceType: codersdk.ResourceTypeUser,
67+
})
68+
require.NoError(t, err)
69+
70+
// Test cases
71+
testCases := []struct {
72+
Name string
73+
SearchQuery string
74+
ExpectedResult int
75+
}{
76+
{
77+
Name: "FilterByCreateAction",
78+
SearchQuery: "action:create",
79+
ExpectedResult: 2,
80+
},
81+
{
82+
Name: "FilterByDeleteAction",
83+
SearchQuery: "action:delete",
84+
ExpectedResult: 1,
85+
},
86+
{
87+
Name: "FilterByUserResourceType",
88+
SearchQuery: "resource_type:user",
89+
ExpectedResult: 2,
90+
},
91+
{
92+
Name: "FilterByTemplateResourceType",
93+
SearchQuery: "resource_type:template",
94+
ExpectedResult: 1,
95+
},
96+
}
97+
98+
for _, testCase := range testCases {
99+
t.Run(testCase.Name, func(t *testing.T) {
100+
t.Parallel()
101+
auditLogs, err := client.AuditLogs(ctx, codersdk.AuditLogsRequest{
102+
SearchQuery: testCase.SearchQuery,
103+
Pagination: codersdk.Pagination{
104+
Limit: 25,
105+
},
106+
})
107+
require.NoError(t, err, "fetch audit logs")
108+
require.Len(t, auditLogs.AuditLogs, testCase.ExpectedResult, "expected audit logs returned")
109+
})
110+
}
111+
})
112+
}

coderd/database/databasefake/databasefake.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2361,6 +2361,14 @@ func (q *fakeQuerier) GetAuditLogsOffset(ctx context.Context, arg database.GetAu
23612361
continue
23622362
}
23632363

2364+
if arg.Action != "" && !strings.Contains(string(alog.Action), arg.Action) {
2365+
continue
2366+
}
2367+
2368+
if arg.ResourceType != "" && !strings.Contains(string(alog.ResourceType), arg.ResourceType) {
2369+
continue
2370+
}
2371+
23642372
user, err := q.GetUserByID(ctx, alog.UserID)
23652373
userValid := err == nil
23662374

coderd/database/queries.sql.go

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

coderd/database/queries/auditlogs.sql

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,19 @@ FROM
1313
audit_logs
1414
LEFT JOIN
1515
users ON audit_logs.user_id = users.id
16+
WHERE
17+
-- Filter resource_type
18+
CASE
19+
WHEN @resource_type :: text != '' THEN
20+
resource_type = @resource_type :: resource_type
21+
ELSE true
22+
END
23+
-- Filter action
24+
AND CASE
25+
WHEN @action :: text != '' THEN
26+
action = @action :: audit_action
27+
ELSE true
28+
END
1629
ORDER BY
1730
"time" DESC
1831
LIMIT

codersdk/audit.go

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"encoding/json"
66
"net/http"
77
"net/netip"
8+
"strings"
89
"time"
910

1011
"github.com/google/uuid"
@@ -93,6 +94,11 @@ type AuditLog struct {
9394
User *User `json:"user"`
9495
}
9596

97+
type AuditLogsRequest struct {
98+
SearchQuery string `json:"q,omitempty"`
99+
Pagination
100+
}
101+
96102
type AuditLogResponse struct {
97103
AuditLogs []AuditLog `json:"audit_logs"`
98104
}
@@ -101,9 +107,22 @@ type AuditLogCountResponse struct {
101107
Count int64 `json:"count"`
102108
}
103109

110+
type CreateTestAuditLogRequest struct {
111+
Action AuditAction `json:"action,omitempty"`
112+
ResourceType ResourceType `json:"resource_type,omitempty"`
113+
}
114+
104115
// AuditLogs retrieves audit logs from the given page.
105-
func (c *Client) AuditLogs(ctx context.Context, page Pagination) (AuditLogResponse, error) {
106-
res, err := c.Request(ctx, http.MethodGet, "/api/v2/audit", nil, page.asRequestOption())
116+
func (c *Client) AuditLogs(ctx context.Context, req AuditLogsRequest) (AuditLogResponse, error) {
117+
res, err := c.Request(ctx, http.MethodGet, "/api/v2/audit", nil, req.Pagination.asRequestOption(), func(r *http.Request) {
118+
q := r.URL.Query()
119+
var params []string
120+
if req.SearchQuery != "" {
121+
params = append(params, req.SearchQuery)
122+
}
123+
q.Set("q", strings.Join(params, " "))
124+
r.URL.RawQuery = q.Encode()
125+
})
107126
if err != nil {
108127
return AuditLogResponse{}, err
109128
}
@@ -143,8 +162,8 @@ func (c *Client) AuditLogCount(ctx context.Context) (AuditLogCountResponse, erro
143162
return logRes, nil
144163
}
145164

146-
func (c *Client) CreateTestAuditLog(ctx context.Context) error {
147-
res, err := c.Request(ctx, http.MethodPost, "/api/v2/audit/testgenerate", nil)
165+
func (c *Client) CreateTestAuditLog(ctx context.Context, req CreateTestAuditLogRequest) error {
166+
res, err := c.Request(ctx, http.MethodPost, "/api/v2/audit/testgenerate", req)
148167
if err != nil {
149168
return err
150169
}

site/src/api/api.ts

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -428,15 +428,21 @@ export const getEntitlements = async (): Promise<TypesGen.Entitlements> => {
428428
return response.data
429429
}
430430

431-
interface GetAuditLogsOptions {
432-
limit: number
433-
offset: number
434-
}
435-
436431
export const getAuditLogs = async (
437-
options: GetAuditLogsOptions,
432+
options: TypesGen.AuditLogsRequest,
438433
): Promise<TypesGen.AuditLogResponse> => {
439-
const response = await axios.get(`/api/v2/audit?limit=${options.limit}&offset=${options.offset}`)
434+
const searchParams = new URLSearchParams()
435+
if (options.limit) {
436+
searchParams.set("limit", options.limit.toString())
437+
}
438+
if (options.offset) {
439+
searchParams.set("offset", options.offset.toString())
440+
}
441+
if (options.q) {
442+
searchParams.set("q", options.q)
443+
}
444+
445+
const response = await axios.get(`/api/v2/audit?${searchParams.toString()}`)
440446
return response.data
441447
}
442448

site/src/api/typesGenerated.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,11 @@ export interface AuditLogResponse {
8686
readonly audit_logs: AuditLog[]
8787
}
8888

89+
// From codersdk/audit.go
90+
export interface AuditLogsRequest extends Pagination {
91+
readonly q?: string
92+
}
93+
8994
// From codersdk/users.go
9095
export interface AuthMethods {
9196
readonly password: boolean
@@ -166,6 +171,12 @@ export interface CreateTemplateVersionRequest {
166171
readonly parameter_values?: CreateParameterRequest[]
167172
}
168173

174+
// From codersdk/audit.go
175+
export interface CreateTestAuditLogRequest {
176+
readonly action?: AuditAction
177+
readonly resource_type?: ResourceType
178+
}
179+
169180
// From codersdk/users.go
170181
export interface CreateUserRequest {
171182
readonly email: string

0 commit comments

Comments
 (0)