Skip to content

Commit e0f839e

Browse files
committed
Pin audit log pagination end_time in page tokens
1 parent f01f6fc commit e0f839e

File tree

3 files changed

+39
-25
lines changed

3 files changed

+39
-25
lines changed

docs/enterprise-api.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -423,7 +423,9 @@ message GetAuditLogResponse {
423423
### Polling Recommendations
424424

425425
- Treat `next_page_token` as opaque. Do not parse it.
426-
- Keep `start_time` / `end_time` fixed while paginating through one window.
426+
- Keep `start_time` / `end_time` fixed while paginating through one window. If
427+
`end_time` is omitted on the first page, the server pins it into the returned
428+
cursor.
427429
- Resume by passing the returned `next_page_token` until it is empty.
428430
- For continuous export, query bounded windows (for example every minute) and checkpoint the last successful window end.
429431

enterprise/server/api/api_server.go

Lines changed: 28 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,10 @@ func (s *APIServer) GetAuditLog(ctx context.Context, req *apipb.GetAuditLogReque
403403
if selector == nil {
404404
selector = &apipb.AuditLogSelector{}
405405
}
406+
pageToken, err := parseAuditLogPageToken(req.GetPageToken())
407+
if err != nil {
408+
return nil, err
409+
}
406410

407411
startTime := selector.GetStartTime()
408412
if startTime == nil {
@@ -422,6 +426,17 @@ func (s *APIServer) GetAuditLog(ctx context.Context, req *apipb.GetAuditLogReque
422426
if startTime.AsTime().After(endTime.AsTime()) {
423427
return nil, status.InvalidArgumentErrorf("start_time must not be after end_time")
424428
}
429+
startUsec := startTime.AsTime().UnixMicro()
430+
endUsec := endTime.AsTime().UnixMicro()
431+
if pageToken != nil {
432+
if selector.GetEndTime() != nil && endUsec != pageToken.EndTimeUsec {
433+
return nil, status.InvalidArgumentError("page_token does not match selector.end_time")
434+
}
435+
endUsec = pageToken.EndTimeUsec
436+
}
437+
if startUsec > endUsec {
438+
return nil, status.InvalidArgumentErrorf("start_time must not be after end_time")
439+
}
425440

426441
if s.env.GetAuditLogger() == nil || s.env.GetOLAPDBHandle() == nil {
427442
return nil, status.UnimplementedError("Audit logger not configured")
@@ -447,19 +462,13 @@ func (s *APIServer) GetAuditLog(ctx context.Context, req *apipb.GetAuditLogReque
447462
pageSize = defaultAuditLogPageSize
448463
}
449464
pageSize = min(pageSize, maxAuditLogPageSize)
450-
startUsec := startTime.AsTime().UnixMicro()
451-
endUsec := endTime.AsTime().UnixMicro()
452465

453466
q := query_builder.NewQuery(`SELECT * FROM AuditLogs`)
454467
q.AddWhereClause("group_id = ?", user.GetGroupID())
455468
q.AddWhereClause("event_time_usec >= ?", startUsec)
456469
q.AddWhereClause("event_time_usec < ?", endUsec)
457-
if req.GetPageToken() != "" {
458-
token, err := parseAuditLogPageToken(req.GetPageToken())
459-
if err != nil {
460-
return nil, err
461-
}
462-
q.AddWhereClause("(event_time_usec, audit_log_id) > (?, ?)", token.EventTimeUsec, token.AuditLogID)
470+
if pageToken != nil {
471+
q.AddWhereClause("(event_time_usec, audit_log_id) > (?, ?)", pageToken.EventTimeUsec, pageToken.AuditLogID)
463472
}
464473
// Match AuditLogs sort key to keep keyset pagination index-friendly.
465474
q.SetOrderBy("group_id, event_time_usec, audit_log_id", true)
@@ -491,6 +500,7 @@ func (s *APIServer) GetAuditLog(ctx context.Context, req *apipb.GetAuditLogReque
491500

492501
rsp.Entry = append(rsp.Entry, entry)
493502
lastReturnedToken = auditLogPageToken{
503+
EndTimeUsec: endUsec,
494504
EventTimeUsec: row.EventTimeUsec,
495505
AuditLogID: row.AuditLogID,
496506
}
@@ -504,6 +514,7 @@ func (s *APIServer) GetAuditLog(ctx context.Context, req *apipb.GetAuditLogReque
504514
}
505515

506516
type auditLogPageToken struct {
517+
EndTimeUsec int64 `json:"end_time_usec"`
507518
EventTimeUsec int64 `json:"event_time_usec"`
508519
AuditLogID string `json:"audit_log_id"`
509520
}
@@ -516,17 +527,20 @@ func encodeAuditLogPageToken(token auditLogPageToken) (string, error) {
516527
return base64.RawURLEncoding.EncodeToString(data), nil
517528
}
518529

519-
func parseAuditLogPageToken(pageToken string) (auditLogPageToken, error) {
530+
func parseAuditLogPageToken(pageToken string) (*auditLogPageToken, error) {
531+
if pageToken == "" {
532+
return nil, nil
533+
}
520534
data, err := base64.RawURLEncoding.DecodeString(pageToken)
521535
if err != nil {
522-
return auditLogPageToken{}, status.InvalidArgumentErrorf("invalid page_token")
536+
return nil, status.InvalidArgumentErrorf("invalid page_token")
523537
}
524-
token := auditLogPageToken{}
525-
if err := json.Unmarshal(data, &token); err != nil {
526-
return auditLogPageToken{}, status.InvalidArgumentErrorf("invalid page_token")
538+
token := &auditLogPageToken{}
539+
if err := json.Unmarshal(data, token); err != nil {
540+
return nil, status.InvalidArgumentErrorf("invalid page_token")
527541
}
528542
if token.AuditLogID == "" {
529-
return auditLogPageToken{}, status.InvalidArgumentErrorf("invalid page_token")
543+
return nil, status.InvalidArgumentErrorf("invalid page_token")
530544
}
531545
return token, nil
532546
}

enterprise/server/api/api_server_test.go

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -312,22 +312,18 @@ func TestGetAuditLog(t *testing.T) {
312312
require.NoError(t, err)
313313
keyCtx := env.GetAuthenticator().AuthContextFromAPIKey(ctx, key.Value)
314314

315+
env.GetAuditLogger().LogForGroup(userCtx, groupID, alpb.Action_UPDATE, &grpb.UpdateGroupRequest{Name: "before-window"})
316+
time.Sleep(2 * time.Millisecond)
317+
windowStart := time.Now()
318+
time.Sleep(2 * time.Millisecond)
315319
env.GetAuditLogger().LogForGroup(userCtx, groupID, alpb.Action_UPDATE, &grpb.UpdateGroupRequest{Name: "update-1"})
316320
env.GetAuditLogger().LogForGroup(userCtx, groupID, alpb.Action_UPDATE, &grpb.UpdateGroupRequest{Name: "update-2"})
317321

318322
s := NewAPIServer(env)
319323
selector := &apipb.AuditLogSelector{
320-
StartTime: timestamppb.New(time.Unix(0, 0)),
321-
EndTime: timestamppb.New(time.Now().Add(time.Minute)),
324+
StartTime: timestamppb.New(windowStart),
322325
}
323326

324-
directRsp, err := env.GetAuditLogger().GetLogs(keyCtx, &alpb.GetAuditLogsRequest{
325-
TimestampAfter: selector.GetStartTime(),
326-
TimestampBefore: selector.GetEndTime(),
327-
})
328-
require.NoError(t, err)
329-
require.Len(t, directRsp.GetEntries(), 2)
330-
331327
page1, err := s.GetAuditLog(keyCtx, &apipb.GetAuditLogRequest{
332328
Selector: selector,
333329
PageSize: 1,
@@ -337,8 +333,10 @@ func TestGetAuditLog(t *testing.T) {
337333
require.NotEmpty(t, page1.GetNextPageToken())
338334
require.NotContains(t, page1.GetNextPageToken(), "|")
339335

336+
time.Sleep(2 * time.Millisecond)
337+
env.GetAuditLogger().LogForGroup(userCtx, groupID, alpb.Action_UPDATE, &grpb.UpdateGroupRequest{Name: "update-3"})
338+
340339
page2, err := s.GetAuditLog(keyCtx, &apipb.GetAuditLogRequest{
341-
Selector: selector,
342340
PageSize: 1,
343341
PageToken: page1.GetNextPageToken(),
344342
})

0 commit comments

Comments
 (0)