mirror of
https://github.com/sourcegraph/sourcegraph.git
synced 2026-02-06 18:51:59 +00:00
Propagates a for-reference-only record of the first `User-Agent` seen when a request gets into Sourcegraph across services and contexts. This allows telemetry to try and indicate where a request originates from (https://github.com/sourcegraph/sourcegraph/pull/64112), rather than only having the most recent user-agent. A new header and `requestclient.Client` property `X-Forwarded-For-User-Agent` and `ForwardedForUserAgent` is used to explicitly forward this. Strictly speaking I think we're supposed to just forward `User-Agent` but it looks like in multiple places we add/clobber the `User-Agent` ourselves. The gRPC propagator currently sets user-agent on outgoing requests, this change also makes that consistent with the HTTP transport, such that both only explicitly propagate `X-Forwarded-For-User-Agent` ## Test plan Unit tests
244 lines
7.2 KiB
Go
244 lines
7.2 KiB
Go
package audit
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
|
|
"github.com/hexops/autogold/v2"
|
|
"github.com/sourcegraph/log"
|
|
"github.com/sourcegraph/log/logtest"
|
|
"github.com/stretchr/testify/assert"
|
|
|
|
"github.com/sourcegraph/sourcegraph/internal/actor"
|
|
"github.com/sourcegraph/sourcegraph/internal/conf"
|
|
"github.com/sourcegraph/sourcegraph/internal/env"
|
|
"github.com/sourcegraph/sourcegraph/internal/requestclient"
|
|
"github.com/sourcegraph/sourcegraph/schema"
|
|
)
|
|
|
|
func TestLog(t *testing.T) {
|
|
testCases := []struct {
|
|
name string
|
|
actor *actor.Actor
|
|
client *requestclient.Client
|
|
additionalContext []log.Field
|
|
expectedEntry autogold.Value
|
|
}{
|
|
{
|
|
name: "fully populated audit data",
|
|
actor: &actor.Actor{UID: 1},
|
|
client: &requestclient.Client{
|
|
IP: "192.168.0.1",
|
|
ForwardedFor: "192.168.0.1",
|
|
UserAgent: "Foobar",
|
|
},
|
|
additionalContext: []log.Field{log.String("additional", "stuff")},
|
|
expectedEntry: autogold.Expect(map[string]interface{}{"additional": "stuff", "audit": map[string]interface{}{
|
|
"action": "test audit action",
|
|
"actor": map[string]interface{}{
|
|
"X-Forwarded-For": "192.168.0.1",
|
|
"actorUID": "1",
|
|
"forwardedForUserAgent": "",
|
|
"ip": "192.168.0.1",
|
|
"userAgent": "Foobar",
|
|
},
|
|
"auditId": "test-audit-id-1234",
|
|
"entity": "test entity",
|
|
}}),
|
|
},
|
|
{
|
|
name: "anonymous actor",
|
|
actor: &actor.Actor{AnonymousUID: "anonymous"},
|
|
client: &requestclient.Client{
|
|
IP: "192.168.0.1",
|
|
ForwardedFor: "192.168.0.1",
|
|
UserAgent: "Foobar",
|
|
},
|
|
additionalContext: []log.Field{log.String("additional", "stuff")},
|
|
expectedEntry: autogold.Expect(map[string]interface{}{"additional": "stuff", "audit": map[string]interface{}{
|
|
"action": "test audit action",
|
|
"actor": map[string]interface{}{
|
|
"X-Forwarded-For": "192.168.0.1",
|
|
"actorUID": "anonymous",
|
|
"forwardedForUserAgent": "",
|
|
"ip": "192.168.0.1",
|
|
"userAgent": "Foobar",
|
|
},
|
|
"auditId": "test-audit-id-1234",
|
|
"entity": "test entity",
|
|
}}),
|
|
},
|
|
{
|
|
name: "missing actor",
|
|
actor: &actor.Actor{ /*missing data*/ },
|
|
client: &requestclient.Client{
|
|
IP: "192.168.0.1",
|
|
ForwardedFor: "192.168.0.1",
|
|
UserAgent: "Foobar",
|
|
},
|
|
additionalContext: []log.Field{log.String("additional", "stuff")},
|
|
expectedEntry: autogold.Expect(map[string]interface{}{"additional": "stuff", "audit": map[string]interface{}{
|
|
"action": "test audit action",
|
|
"actor": map[string]interface{}{
|
|
"X-Forwarded-For": "192.168.0.1",
|
|
"actorUID": "unknown",
|
|
"forwardedForUserAgent": "",
|
|
"ip": "192.168.0.1",
|
|
"userAgent": "Foobar",
|
|
},
|
|
"auditId": "test-audit-id-1234",
|
|
"entity": "test entity",
|
|
}}),
|
|
},
|
|
{
|
|
name: "missing client info",
|
|
actor: &actor.Actor{UID: 1},
|
|
client: nil,
|
|
additionalContext: []log.Field{log.String("additional", "stuff")},
|
|
expectedEntry: autogold.Expect(map[string]interface{}{"additional": "stuff", "audit": map[string]interface{}{
|
|
"action": "test audit action",
|
|
"actor": map[string]interface{}{
|
|
"X-Forwarded-For": "unknown",
|
|
"actorUID": "1",
|
|
"forwardedForUserAgent": "unknown",
|
|
"ip": "unknown",
|
|
"userAgent": "unknown",
|
|
},
|
|
"auditId": "test-audit-id-1234",
|
|
"entity": "test entity",
|
|
}}),
|
|
},
|
|
{
|
|
name: "no additional context",
|
|
actor: &actor.Actor{UID: 1},
|
|
client: &requestclient.Client{
|
|
IP: "192.168.0.1",
|
|
ForwardedFor: "192.168.0.1",
|
|
UserAgent: "Foobar",
|
|
},
|
|
additionalContext: nil,
|
|
expectedEntry: autogold.Expect(map[string]interface{}{"audit": map[string]interface{}{
|
|
"action": "test audit action", "actor": map[string]interface{}{
|
|
"X-Forwarded-For": "192.168.0.1",
|
|
"actorUID": "1",
|
|
"forwardedForUserAgent": "",
|
|
"ip": "192.168.0.1",
|
|
"userAgent": "Foobar",
|
|
},
|
|
"auditId": "test-audit-id-1234",
|
|
"entity": "test entity",
|
|
}}),
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
ctx := context.Background()
|
|
ctx = actor.WithActor(ctx, tc.actor)
|
|
ctx = requestclient.WithClient(ctx, tc.client)
|
|
|
|
fields := Record{
|
|
Entity: "test entity",
|
|
Action: "test audit action",
|
|
Fields: tc.additionalContext,
|
|
|
|
auditIDGenerator: func() string { return "test-audit-id-1234" },
|
|
}
|
|
|
|
logger, exportLogs := logtest.Captured(t)
|
|
|
|
Log(ctx, logger, fields)
|
|
|
|
logs := exportLogs()
|
|
if len(logs) != 1 {
|
|
t.Fatal("expected to capture one log exactly")
|
|
}
|
|
|
|
assert.Contains(t, logs[0].Message, "test audit action (sampling immunity token")
|
|
|
|
// non-audit fields are preserved
|
|
tc.expectedEntry.Equal(t, logs[0].Fields)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestIsEnabled(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
cfg schema.SiteConfiguration
|
|
expected map[AuditLogSetting]bool
|
|
}{
|
|
{
|
|
name: "empty log results in default audit log settings",
|
|
cfg: schema.SiteConfiguration{},
|
|
expected: map[AuditLogSetting]bool{GitserverAccess: false, InternalTraffic: false, GraphQL: false},
|
|
},
|
|
{
|
|
name: "empty audit log config results in default audit log settings",
|
|
cfg: schema.SiteConfiguration{Log: &schema.Log{}},
|
|
expected: map[AuditLogSetting]bool{GitserverAccess: false, InternalTraffic: false, GraphQL: false},
|
|
},
|
|
{
|
|
name: "fully populated audit log is read correctly",
|
|
cfg: schema.SiteConfiguration{
|
|
Log: &schema.Log{
|
|
AuditLog: &schema.AuditLog{
|
|
InternalTraffic: true,
|
|
GitserverAccess: true,
|
|
GraphQL: true,
|
|
}}},
|
|
expected: map[AuditLogSetting]bool{GitserverAccess: true, InternalTraffic: true, GraphQL: true},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
for setting, want := range tt.expected {
|
|
assert.Equalf(t, want, IsEnabled(tt.cfg, setting), "IsEnabled(%v, %v)", tt.cfg, setting)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// Remove when deprecated audit log schema.Log.AuditLog.SeverityLevel is removed.
|
|
func TestSwitchingSeverityLevelDoesNothing(t *testing.T) {
|
|
useAuditLogLevel("INFO")
|
|
defer conf.Mock(nil)
|
|
|
|
logs := auditLogMessage(t)
|
|
assert.Equal(t, 1, len(logs))
|
|
assert.Equal(t, log.Level(env.LogLevel), logs[0].Level)
|
|
|
|
useAuditLogLevel("WARN")
|
|
logs = auditLogMessage(t)
|
|
assert.Equal(t, 1, len(logs))
|
|
assert.Equal(t, log.Level(env.LogLevel), logs[0].Level)
|
|
}
|
|
|
|
func useAuditLogLevel(level string) {
|
|
conf.Mock(&conf.Unified{SiteConfiguration: schema.SiteConfiguration{
|
|
Log: &schema.Log{
|
|
AuditLog: &schema.AuditLog{
|
|
InternalTraffic: true,
|
|
GitserverAccess: true,
|
|
GraphQL: true,
|
|
SeverityLevel: level,
|
|
}}}})
|
|
}
|
|
|
|
func auditLogMessage(t *testing.T) []logtest.CapturedLog {
|
|
ctx := context.Background()
|
|
ctx = actor.WithActor(ctx, &actor.Actor{UID: 1})
|
|
ctx = requestclient.WithClient(ctx, &requestclient.Client{IP: "192.168.1.1"})
|
|
|
|
record := Record{
|
|
Entity: "test entity",
|
|
Action: "test audit action",
|
|
Fields: nil,
|
|
}
|
|
|
|
logger, exportLogs := logtest.Captured(t)
|
|
Log(ctx, logger, record)
|
|
|
|
return exportLogs()
|
|
}
|