1
1
package coderd
2
2
3
3
import (
4
+ "context"
4
5
"database/sql"
5
6
"encoding/json"
6
7
"fmt"
@@ -13,7 +14,9 @@ import (
13
14
14
15
"github.com/google/uuid"
15
16
"github.com/tabbed/pqtype"
17
+ "golang.org/x/xerrors"
16
18
19
+ "cdr.dev/slog"
17
20
"github.com/coder/coder/coderd/database"
18
21
"github.com/coder/coder/coderd/httpapi"
19
22
"github.com/coder/coder/coderd/httpmw"
@@ -69,7 +72,7 @@ func (api *API) auditLogs(rw http.ResponseWriter, r *http.Request) {
69
72
}
70
73
71
74
httpapi .Write (ctx , rw , http .StatusOK , codersdk.AuditLogResponse {
72
- AuditLogs : convertAuditLogs (dblogs ),
75
+ AuditLogs : api . convertAuditLogs (ctx , dblogs ),
73
76
Count : dblogs [0 ].Count ,
74
77
})
75
78
}
@@ -147,17 +150,17 @@ func (api *API) generateFakeAuditLog(rw http.ResponseWriter, r *http.Request) {
147
150
rw .WriteHeader (http .StatusNoContent )
148
151
}
149
152
150
- func convertAuditLogs (dblogs []database.GetAuditLogsOffsetRow ) []codersdk.AuditLog {
153
+ func ( api * API ) convertAuditLogs (ctx context. Context , dblogs []database.GetAuditLogsOffsetRow ) []codersdk.AuditLog {
151
154
alogs := make ([]codersdk.AuditLog , 0 , len (dblogs ))
152
155
153
156
for _ , dblog := range dblogs {
154
- alogs = append (alogs , convertAuditLog (dblog ))
157
+ alogs = append (alogs , api . convertAuditLog (ctx , dblog ))
155
158
}
156
159
157
160
return alogs
158
161
}
159
162
160
- func convertAuditLog (dblog database.GetAuditLogsOffsetRow ) codersdk.AuditLog {
163
+ func ( api * API ) convertAuditLog (ctx context. Context , dblog database.GetAuditLogsOffsetRow ) codersdk.AuditLog {
161
164
ip , _ := netip .AddrFromSlice (dblog .Ip .IPNet .IP )
162
165
163
166
diff := codersdk.AuditDiff {}
@@ -182,6 +185,14 @@ func convertAuditLog(dblog database.GetAuditLogsOffsetRow) codersdk.AuditLog {
182
185
}
183
186
}
184
187
188
+ isDeleted := api .auditLogIsResourceDeleted (ctx , dblog )
189
+ var resourceLink string
190
+ if isDeleted {
191
+ resourceLink = ""
192
+ } else {
193
+ resourceLink = api .auditLogResourceLink (ctx , dblog )
194
+ }
195
+
185
196
return codersdk.AuditLog {
186
197
ID : dblog .ID ,
187
198
RequestID : dblog .RequestID ,
@@ -197,34 +208,123 @@ func convertAuditLog(dblog database.GetAuditLogsOffsetRow) codersdk.AuditLog {
197
208
Diff : diff ,
198
209
StatusCode : dblog .StatusCode ,
199
210
AdditionalFields : dblog .AdditionalFields ,
200
- Description : auditLogDescription (dblog ),
201
211
User : user ,
212
+ Description : auditLogDescription (dblog ),
213
+ ResourceLink : resourceLink ,
214
+ IsDeleted : isDeleted ,
202
215
}
203
216
}
204
217
205
218
func auditLogDescription (alog database.GetAuditLogsOffsetRow ) string {
206
- str := fmt .Sprintf ("{user} %s %s " ,
219
+ str := fmt .Sprintf ("{user} %s" ,
207
220
codersdk .AuditAction (alog .Action ).FriendlyString (),
208
- codersdk .ResourceType (alog .ResourceType ).FriendlyString (),
209
221
)
210
222
211
- // Strings for workspace_builds follow the below format:
212
- // "{user} started workspace build for {target}"
213
- // where target is a workspace instead of the workspace build,
223
+ // Strings for starting/stopping workspace builds follow the below format:
224
+ // "{user} started build for workspace {target}"
225
+ // where target is a workspace instead of a workspace build
214
226
// passed in on the FE via AuditLog.AdditionalFields rather than derived in request.go:35
215
- if alog .ResourceType == database .ResourceTypeWorkspaceBuild {
216
- str += " for"
227
+ if alog .ResourceType == database .ResourceTypeWorkspaceBuild && alog . Action != database . AuditActionDelete {
228
+ str += " build for"
217
229
}
218
230
219
- // We don't display the name for git ssh keys. It's fairly long and doesn't
231
+ // We don't display the name (target) for git ssh keys. It's fairly long and doesn't
220
232
// make too much sense to display.
221
- if alog .ResourceType != database .ResourceTypeGitSshKey {
222
- str += " {target}"
233
+ if alog .ResourceType == database .ResourceTypeGitSshKey {
234
+ str += fmt .Sprintf (" the %s" ,
235
+ codersdk .ResourceType (alog .ResourceType ).FriendlyString ())
236
+ return str
223
237
}
224
238
239
+ str += fmt .Sprintf (" %s" ,
240
+ codersdk .ResourceType (alog .ResourceType ).FriendlyString ())
241
+
242
+ str += " {target}"
243
+
225
244
return str
226
245
}
227
246
247
+ func (api * API ) auditLogIsResourceDeleted (ctx context.Context , alog database.GetAuditLogsOffsetRow ) bool {
248
+ switch alog .ResourceType {
249
+ case database .ResourceTypeTemplate :
250
+ template , err := api .Database .GetTemplateByID (ctx , alog .ResourceID )
251
+ if err != nil {
252
+ if xerrors .Is (err , sql .ErrNoRows ) {
253
+ return true
254
+ }
255
+ api .Logger .Error (ctx , "fetch template" , slog .Error (err ))
256
+ }
257
+ return template .Deleted
258
+ case database .ResourceTypeUser :
259
+ user , err := api .Database .GetUserByID (ctx , alog .ResourceID )
260
+ if err != nil {
261
+ if xerrors .Is (err , sql .ErrNoRows ) {
262
+ return true
263
+ }
264
+ api .Logger .Error (ctx , "fetch user" , slog .Error (err ))
265
+ }
266
+ return user .Deleted
267
+ case database .ResourceTypeWorkspace :
268
+ workspace , err := api .Database .GetWorkspaceByID (ctx , alog .ResourceID )
269
+ if err != nil {
270
+ if xerrors .Is (err , sql .ErrNoRows ) {
271
+ return true
272
+ }
273
+ api .Logger .Error (ctx , "fetch workspace" , slog .Error (err ))
274
+ }
275
+ return workspace .Deleted
276
+ case database .ResourceTypeWorkspaceBuild :
277
+ workspaceBuild , err := api .Database .GetWorkspaceBuildByID (ctx , alog .ResourceID )
278
+ if err != nil {
279
+ if xerrors .Is (err , sql .ErrNoRows ) {
280
+ return true
281
+ }
282
+ api .Logger .Error (ctx , "fetch workspace build" , slog .Error (err ))
283
+ }
284
+ // We use workspace as a proxy for workspace build here
285
+ workspace , err := api .Database .GetWorkspaceByID (ctx , workspaceBuild .WorkspaceID )
286
+ if err != nil {
287
+ if xerrors .Is (err , sql .ErrNoRows ) {
288
+ return true
289
+ }
290
+ api .Logger .Error (ctx , "fetch workspace" , slog .Error (err ))
291
+ }
292
+ return workspace .Deleted
293
+ default :
294
+ return false
295
+ }
296
+ }
297
+
298
+ type AdditionalFields struct {
299
+ WorkspaceName string
300
+ BuildNumber string
301
+ }
302
+
303
+ func (api * API ) auditLogResourceLink (ctx context.Context , alog database.GetAuditLogsOffsetRow ) string {
304
+ switch alog .ResourceType {
305
+ case database .ResourceTypeTemplate :
306
+ return fmt .Sprintf ("/templates/%s" ,
307
+ alog .ResourceTarget )
308
+ case database .ResourceTypeUser :
309
+ return fmt .Sprintf ("/users?filter=%s" ,
310
+ alog .ResourceTarget )
311
+ case database .ResourceTypeWorkspace :
312
+ return fmt .Sprintf ("/@%s/%s" ,
313
+ alog .UserUsername .String , alog .ResourceTarget )
314
+ case database .ResourceTypeWorkspaceBuild :
315
+ additionalFieldsBytes := []byte (alog .AdditionalFields )
316
+ var additionalFields AdditionalFields
317
+ err := json .Unmarshal (additionalFieldsBytes , & additionalFields )
318
+ if err != nil {
319
+ api .Logger .Error (ctx , "unmarshal workspace name" , slog .Error (err ))
320
+ }
321
+ return fmt .Sprintf ("/@%s/%s/builds/%s" ,
322
+ alog .UserUsername .String , additionalFields .WorkspaceName , additionalFields .BuildNumber )
323
+ default :
324
+ return ""
325
+ }
326
+ }
327
+
228
328
// auditSearchQuery takes a query string and returns the auditLog filter.
229
329
// It also can return the list of validation errors to return to the api.
230
330
func auditSearchQuery (query string ) (database.GetAuditLogsOffsetParams , []codersdk.ValidationError ) {
0 commit comments