diff --git a/ddtrace/tracer/option.go b/ddtrace/tracer/option.go index 2dc38796ad..db683f6c14 100644 --- a/ddtrace/tracer/option.go +++ b/ddtrace/tracer/option.go @@ -829,3 +829,41 @@ func StackFrames(n, skip uint) FinishOption { cfg.SkipStackFrames = skip } } + +// UserMonitoringOption represents a function that can be provided as a parameter to SetUser. +type UserMonitoringOption func(Span) + +// WithUserEmail returns the option setting the email of the authenticated user. +func WithUserEmail(email string) UserMonitoringOption { + return func(s Span) { + s.SetTag("usr.email", email) + } +} + +// WithUserName returns the option setting the name of the authenticated user. +func WithUserName(name string) UserMonitoringOption { + return func(s Span) { + s.SetTag("usr.name", name) + } +} + +// WithUserSessionID returns the option setting the session ID of the authenticated user. +func WithUserSessionID(sessionID string) UserMonitoringOption { + return func(s Span) { + s.SetTag("usr.session_id", sessionID) + } +} + +// WithUserRole returns the option setting the role of the authenticated user. +func WithUserRole(role string) UserMonitoringOption { + return func(s Span) { + s.SetTag("usr.role", role) + } +} + +// WithUserScope returns the option setting the scope (authorizations) of the authenticated user +func WithUserScope(scope string) UserMonitoringOption { + return func(s Span) { + s.SetTag("usr.scope", scope) + } +} diff --git a/ddtrace/tracer/tracer.go b/ddtrace/tracer/tracer.go index 8532f5cbcd..d1b4ff9772 100644 --- a/ddtrace/tracer/tracer.go +++ b/ddtrace/tracer/tracer.go @@ -153,6 +153,23 @@ func Inject(ctx ddtrace.SpanContext, carrier interface{}) error { return internal.GetGlobalTracer().Inject(ctx, carrier) } +// SetUser associates user information to the current trace which the +// provided span belongs to. The options can be used to tune which user +// bit of information gets monitored. +func SetUser(s Span, id string, opts ...UserMonitoringOption) { + if s == nil { + return + } + if span, ok := s.(*span); ok && span.context != nil { + span = span.context.trace.root + s = span + } + s.SetTag("usr.id", id) + for _, fn := range opts { + fn(s) + } +} + // payloadQueueSize is the buffer size of the trace channel. const payloadQueueSize = 1000 diff --git a/ddtrace/tracer/tracer_test.go b/ddtrace/tracer/tracer_test.go index 4be33eb93c..164f35e7b5 100644 --- a/ddtrace/tracer/tracer_test.go +++ b/ddtrace/tracer/tracer_test.go @@ -1694,6 +1694,47 @@ func TestTakeStackTrace(t *testing.T) { }) } +func TestUserMonitoring(t *testing.T) { + const id = "john.doe#12345" + const name = "John Doe" + const email = "john.doe@hostname.com" + const scope = "read:message, write:files" + const role = "admin" + const sessionID = "session#12345" + expected := []struct{ key, value string }{ + {key: "usr.id", value: id}, + {key: "usr.name", value: name}, + {key: "usr.email", value: email}, + {key: "usr.scope", value: scope}, + {key: "usr.role", value: role}, + {key: "usr.session_id", value: sessionID}, + } + tr := newTracer() + defer tr.Stop() + + t.Run("root", func(t *testing.T) { + s := tr.newRootSpan("root", "test", "test") + SetUser(s, id, WithUserEmail(email), WithUserName(name), WithUserScope(scope), + WithUserRole(role), WithUserSessionID(sessionID)) + s.Finish() + for _, pair := range expected { + assert.Equal(t, pair.value, s.Meta[pair.key]) + } + }) + + t.Run("nested", func(t *testing.T) { + root := tr.newRootSpan("root", "test", "test") + child := tr.newChildSpan("child", root) + SetUser(child, id, WithUserEmail(email), WithUserName(name), WithUserScope(scope), + WithUserRole(role), WithUserSessionID(sessionID)) + child.Finish() + root.Finish() + for _, pair := range expected { + assert.Equal(t, pair.value, root.Meta[pair.key]) + } + }) +} + // BenchmarkTracerStackFrames tests the performance of taking stack trace. func BenchmarkTracerStackFrames(b *testing.B) { tracer, _, _, stop := startTestTracer(b, WithSampler(NewRateSampler(0))) diff --git a/internal/appsec/config.go b/internal/appsec/config.go index 7c66378a88..b4eaf02060 100644 --- a/internal/appsec/config.go +++ b/internal/appsec/config.go @@ -11,6 +11,8 @@ import ( "os" "strconv" "time" + "unicode" + "unicode/utf8" "gopkg.in/DataDog/dd-trace-go.v1/internal/log" ) @@ -69,9 +71,18 @@ func readWAFTimeoutConfig() (timeout time.Duration) { if value == "" { return } + + // Check if the value ends with a letter, which means the user has + // specified their own time duration unit(s) such as 1s200ms. + // Otherwise, default to microseconds. + if lastRune, _ := utf8.DecodeLastRuneInString(value); !unicode.IsLetter(lastRune) { + value += "us" // Add the default microsecond time-duration suffix + } + parsed, err := time.ParseDuration(value) if err != nil { logEnvVarParsingError(wafTimeoutEnvVar, value, err, timeout) + return } if parsed <= 0 { logUnexpectedEnvVarValue(wafTimeoutEnvVar, parsed, "expecting a strictly positive duration", timeout) diff --git a/internal/appsec/config_test.go b/internal/appsec/config_test.go index 6ea9a40702..6e64bac370 100644 --- a/internal/appsec/config_test.go +++ b/internal/appsec/config_test.go @@ -50,6 +50,23 @@ func TestConfig(t *testing.T) { ) }) + t.Run("parsable-default-microsecond", func(t *testing.T) { + restoreEnv := cleanEnv() + defer restoreEnv() + require.NoError(t, os.Setenv(wafTimeoutEnvVar, "1")) + cfg, err := newConfig() + require.NoError(t, err) + require.Equal( + t, + &config{ + rules: []byte(staticRecommendedRule), + wafTimeout: 1 * time.Microsecond, + traceRateLimit: defaultTraceRate, + }, + cfg, + ) + }) + t.Run("not-parsable", func(t *testing.T) { restoreEnv := cleanEnv() defer restoreEnv()