-
Notifications
You must be signed in to change notification settings - Fork 4
feat(audit): session ID generation, sequence counter, and BoundaryLog wiring #196
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
f98634a
2bcedd2
57c9456
cc80579
852f0e3
3036c49
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| package audit | ||
|
|
||
| import "sync/atomic" | ||
|
|
||
| // SequenceCounter is a monotonically increasing counter that assigns a | ||
| // unique sequence number to every audit event within a single boundary | ||
| // session. The counter starts at 0 and is safe for concurrent use by | ||
| // both the socket auditor and the proxy. | ||
| type SequenceCounter struct { | ||
| next atomic.Int32 | ||
| } | ||
|
|
||
| // Next returns the next sequence number. The first call returns 0, | ||
| // subsequent calls return 1, 2, 3, etc. It is safe for concurrent | ||
| // use. | ||
| func (c *SequenceCounter) Next() int32 { | ||
| // Add returns the new value after incrementing, so subtract 1 | ||
| // to produce a zero-based sequence. | ||
| return c.next.Add(1) - 1 | ||
| } |
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: This is basically testing |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| package audit | ||
|
|
||
| import ( | ||
| "testing" | ||
| ) | ||
|
|
||
| func TestSequenceCounter_StartsAtZero(t *testing.T) { | ||
| t.Parallel() | ||
|
|
||
| var c SequenceCounter | ||
| if got := c.Next(); got != 0 { | ||
| t.Fatalf("first call: got %d, want 0", got) | ||
| } | ||
| } | ||
|
|
||
| func TestSequenceCounter_Increments(t *testing.T) { | ||
| t.Parallel() | ||
|
|
||
| var c SequenceCounter | ||
| for i := range int32(100) { | ||
| if got := c.Next(); got != i { | ||
| t.Fatalf("call %d: got %d, want %d", i, got, i) | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -11,6 +11,8 @@ import ( | |
| "testing" | ||
| "time" | ||
|
|
||
| "github.com/google/uuid" | ||
|
|
||
| "github.com/coder/coder/v2/agent/boundarylogproxy/codec" | ||
| agentproto "github.com/coder/coder/v2/agent/proto" | ||
| ) | ||
|
|
@@ -33,6 +35,15 @@ func TestSocketAuditor_AuditRequest_QueuesLog(t *testing.T) { | |
| if log.Allowed != true { | ||
| t.Errorf("expected Allowed=true, got %v", log.Allowed) | ||
| } | ||
| if log.Time == nil { | ||
| t.Fatal("expected Time to be set") | ||
| } | ||
|
Comment on lines
+38
to
+40
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. also validate that it's not the zero time |
||
| if log.Time.AsTime().IsZero() { | ||
| t.Fatal("expected Time to not be the zero time") | ||
| } | ||
| if log.SequenceNumber != 0 { | ||
| t.Errorf("expected first SequenceNumber=0, got %d", log.SequenceNumber) | ||
| } | ||
| httpReq := log.GetHttpRequest() | ||
| if httpReq == nil { | ||
| t.Fatal("expected HttpRequest, got nil") | ||
|
|
@@ -230,6 +241,8 @@ func TestSocketAuditor_Loop_RetriesOnConnectionFailure(t *testing.T) { | |
| logCh: make(chan *agentproto.BoundaryLog, 2*defaultBatchSize), | ||
| batchSize: defaultBatchSize, | ||
| batchTimerDuration: time.Hour, // Ensure timer doesn't interfere with the test | ||
| sessionID: uuid.MustParse("00000000-0000-4000-8000-000000000001"), | ||
| seq: &SequenceCounter{}, | ||
| } | ||
|
|
||
| // Set up hook to detect flush attempts | ||
|
|
@@ -349,6 +362,8 @@ func TestSocketAuditor_Loop_ReportsBatchFullDrops(t *testing.T) { | |
| logCh: make(chan *agentproto.BoundaryLog, 2*defaultBatchSize), | ||
| batchSize: defaultBatchSize, | ||
| batchTimerDuration: time.Hour, | ||
| sessionID: uuid.MustParse("00000000-0000-4000-8000-000000000001"), | ||
| seq: &SequenceCounter{}, | ||
| } | ||
|
|
||
| flushed := make(chan struct{}, 4) | ||
|
|
@@ -451,17 +466,75 @@ func TestSocketAuditor_Loop_ShutdownFlushIncludesDrops(t *testing.T) { | |
| func TestFlush_EmptyBatch(t *testing.T) { | ||
| t.Parallel() | ||
|
|
||
| err := flush(nil, nil) | ||
| err := flush(nil, uuid.Nil, nil) | ||
| if err != nil { | ||
| t.Errorf("expected nil error for empty batch, got %v", err) | ||
| } | ||
|
|
||
| err = flush(nil, []*agentproto.BoundaryLog{}) | ||
| err = flush(nil, uuid.Nil, []*agentproto.BoundaryLog{}) | ||
| if err != nil { | ||
| t.Errorf("expected nil error for empty slice, got %v", err) | ||
| } | ||
| } | ||
|
|
||
| func TestSocketAuditor_AuditRequest_SequenceNumberIncrements(t *testing.T) { | ||
| t.Parallel() | ||
|
|
||
| auditor := setupSocketAuditor(t) | ||
|
|
||
| for i := range 5 { | ||
| auditor.AuditRequest(Request{Method: "GET", URL: "https://example.com", Allowed: true}) | ||
|
|
||
| select { | ||
| case log := <-auditor.logCh: | ||
| if log.SequenceNumber != int32(i) { | ||
| t.Errorf("request %d: expected SequenceNumber=%d, got %d", i, i, log.SequenceNumber) | ||
| } | ||
| default: | ||
| t.Fatalf("request %d: expected log in channel, got none", i) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| func TestSocketAuditor_Loop_FlushIncludesSessionID(t *testing.T) { | ||
| t.Parallel() | ||
|
|
||
| auditor, serverConn := setupTestAuditor(t) | ||
| auditor.batchTimerDuration = time.Hour | ||
|
|
||
| cr := newConnReader() | ||
| go readFromConn(t, serverConn, cr) | ||
|
|
||
| go auditor.Loop(t.Context()) | ||
|
|
||
| // Fill a batch to trigger a flush. | ||
| for i := 0; i < auditor.batchSize; i++ { | ||
| auditor.AuditRequest(Request{Method: "GET", URL: "https://example.com", Allowed: true}) | ||
| } | ||
|
|
||
| select { | ||
| case req := <-cr.logs: | ||
| expectedSessionID := uuid.MustParse("00000000-0000-4000-8000-000000000001").String() | ||
| if req.SessionId != expectedSessionID { | ||
| t.Errorf("expected SessionId=%s, got %q", expectedSessionID, req.SessionId) | ||
| } | ||
| if len(req.Logs) != auditor.batchSize { | ||
| t.Errorf("expected %d logs, got %d", auditor.batchSize, len(req.Logs)) | ||
| } | ||
| // Verify sequence numbers are monotonically increasing. | ||
| for i, log := range req.Logs { | ||
| if log.SequenceNumber != int32(i) { | ||
| t.Errorf("log %d: expected SequenceNumber=%d, got %d", i, i, log.SequenceNumber) | ||
| } | ||
| if log.Time == nil { | ||
| t.Errorf("log %d: expected Time to be set", i) | ||
| } | ||
| } | ||
| case <-time.After(5 * time.Second): | ||
| t.Fatal("timeout waiting for flush") | ||
| } | ||
| } | ||
|
|
||
| // setupSocketAuditor creates a SocketAuditor for tests that only exercise | ||
| // the queueing behavior (no connection needed). | ||
| func setupSocketAuditor(t *testing.T) *SocketAuditor { | ||
|
|
@@ -475,6 +548,8 @@ func setupSocketAuditor(t *testing.T) *SocketAuditor { | |
| logCh: make(chan *agentproto.BoundaryLog, 2*defaultBatchSize), | ||
| batchSize: defaultBatchSize, | ||
| batchTimerDuration: defaultBatchTimerDuration, | ||
| sessionID: uuid.MustParse("00000000-0000-4000-8000-000000000001"), | ||
| seq: &SequenceCounter{}, | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -504,6 +579,8 @@ func setupTestAuditor(t *testing.T) (*SocketAuditor, net.Conn) { | |
| logCh: make(chan *agentproto.BoundaryLog, 2*defaultBatchSize), | ||
| batchSize: defaultBatchSize, | ||
| batchTimerDuration: defaultBatchTimerDuration, | ||
| sessionID: uuid.MustParse("00000000-0000-4000-8000-000000000001"), | ||
| seq: &SequenceCounter{}, | ||
| } | ||
|
|
||
| return auditor, serverConn | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This isn't currently included in the log auditor. Only the socket auditor. Does that matter? I don't think so, but I'm open to the idea.