Skip to content

Commit b292273

Browse files
authored
[AppSec] Api to allow user data to be associated with the local root span (DataDog#2546)
This PR adds a convenient method to the tracer to link an actor to a trace. You have to pass a UserDetails object with at least its Id property set to a non-null value. To correlate users to web requests, you would need to use the following code snippet within each web request, after user authorization has been performed: ```csharp using Datadog.Trace; // ... var userDetails = new UserDetails() { // the systems internal identifier for the users Id = "d41452f2-483d-4082-8728-171a3570e930", // the email address of the user Email = "test@adventure-works.com", // the user's name, as displayed by the system Name = "Jane Doh", // the user's session id SessionId = "d0632156-132b-4baa-95b2-a492c5f9cb16", // the role the user is making the request under Role = "standard", }; Tracer.Instance.ActiveScope?.Span.SetUser(userDetails); ```
1 parent 660ae6e commit b292273

File tree

6 files changed

+329
-0
lines changed

6 files changed

+329
-0
lines changed

docs/Datadog.Trace/README.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,35 @@ Note: Call-target instrumentation does not support instrumenting custom implemen
277277

278278
The `integrations.json` file is no longer required for instrumentation. You can remove references to this file, for example by deleting the `DD_INTEGRATIONS` environment variable.
279279

280+
### User Identification
281+
282+
The tracer provides a convenient method to link an actor to a trace. You have to pass a `UserDetails` object with at least its Id property set to a non-null value.
283+
284+
To correlate users to web requests, you would need to use the following code snippet within each web request, after user authorization has been performed:
285+
286+
```csharp
287+
using Datadog.Trace;
288+
289+
// ...
290+
291+
var userDetails = new UserDetails()
292+
{
293+
// the systems internal identifier for the users
294+
Id = "d41452f2-483d-4082-8728-171a3570e930",
295+
// the email address of the user
296+
Email = "test@adventure-works.com",
297+
// the user's name, as displayed by the system
298+
Name = "Jane Doh",
299+
// the user's session id
300+
SessionId = "d0632156-132b-4baa-95b2-a492c5f9cb16",
301+
// the role the user is making the request under
302+
Role = "standard",
303+
};
304+
Tracer.Instance.ActiveScope?.Span.SetUser(userDetails);
305+
306+
```
307+
280308
## Get in touch
281309

282310
If you have questions, feedback, or feature requests, reach our [support](https://docs.datadoghq.com/help).
311+
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
// <copyright file="SpanExtensions.cs" company="Datadog">
2+
// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License.
3+
// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc.
4+
// </copyright>
5+
6+
using Datadog.Trace.Util;
7+
8+
namespace Datadog.Trace
9+
{
10+
/// <summary>
11+
/// Extension methods for the <see cref="ISpan"/> interface
12+
/// </summary>
13+
public static class SpanExtensions
14+
{
15+
/// <summary>
16+
/// Sets the details of the user on the local root span
17+
/// </summary>
18+
/// <param name="span">The span to be tagged</param>
19+
/// <param name="userDetails">The details of the current logged on user</param>
20+
public static void SetUser(this ISpan span, UserDetails userDetails)
21+
{
22+
if (span is null)
23+
{
24+
ThrowHelper.ThrowArgumentNullException(nameof(span));
25+
}
26+
27+
if (userDetails is null)
28+
{
29+
ThrowHelper.ThrowArgumentNullException(nameof(userDetails));
30+
}
31+
32+
if (userDetails.Id is null)
33+
{
34+
ThrowHelper.ThrowArgumentException(nameof(userDetails) + ".Id must be set to a value other than null", nameof(userDetails));
35+
}
36+
37+
var localRootSpan = span;
38+
if (span is Span spanClass)
39+
{
40+
localRootSpan = spanClass.Context.TraceContext?.RootSpan ?? span;
41+
}
42+
43+
if (userDetails.Id is not null)
44+
{
45+
localRootSpan.SetTag(Tags.User.Id, userDetails.Id);
46+
}
47+
48+
if (userDetails.Email is not null)
49+
{
50+
localRootSpan.SetTag(Tags.User.Email, userDetails.Email);
51+
}
52+
53+
if (userDetails.Name is not null)
54+
{
55+
localRootSpan.SetTag(Tags.User.Name, userDetails.Name);
56+
}
57+
58+
if (userDetails.SessionId is not null)
59+
{
60+
localRootSpan.SetTag(Tags.User.SessionId, userDetails.SessionId);
61+
}
62+
63+
if (userDetails.Role is not null)
64+
{
65+
localRootSpan.SetTag(Tags.User.Role, userDetails.Role);
66+
}
67+
}
68+
}
69+
}

tracer/src/Datadog.Trace/Tags.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -478,5 +478,14 @@ public static class Tags
478478
internal const string GrpcMethodService = "grpc.method.service";
479479
internal const string GrpcMethodName = "grpc.method.name";
480480
internal const string GrpcStatusCode = "grpc.status.code";
481+
482+
internal static class User
483+
{
484+
internal const string Email = "usr.email";
485+
internal const string Name = "usr.name";
486+
internal const string Id = "usr.id";
487+
internal const string SessionId = "usr.session_id";
488+
internal const string Role = "usr.role";
489+
}
481490
}
482491
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// <copyright file="UserDetails.cs" company="Datadog">
2+
// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License.
3+
// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc.
4+
// </copyright>
5+
6+
namespace Datadog.Trace
7+
{
8+
/// <summary>
9+
/// A data container class for the users details
10+
/// </summary>
11+
public class UserDetails
12+
{
13+
/// <summary>
14+
/// Gets or sets the user's email address
15+
/// </summary>
16+
public string Email { get; set; }
17+
18+
/// <summary>
19+
/// Gets or sets the user's name as displayed in the UI
20+
/// </summary>
21+
public string Name { get; set; }
22+
23+
/// <summary>
24+
/// Gets or sets the unique identifier assoicated with the users
25+
/// </summary>
26+
public string Id { get; set; }
27+
28+
/// <summary>
29+
/// Gets or sets the user's session unique identifier
30+
/// </summary>
31+
public string SessionId { get; set; }
32+
33+
/// <summary>
34+
/// Gets or sets the role associated with the user
35+
/// </summary>
36+
public string Role { get; set; }
37+
}
38+
}

tracer/test/Datadog.Trace.Tests/Snapshots/PublicApiTests.PublicApiHasNotChanged.verified.txt

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,10 @@ namespace Datadog.Trace
250250
public Datadog.Trace.ISpanContext Parent { get; set; }
251251
public System.DateTimeOffset? StartTime { get; set; }
252252
}
253+
public static class SpanExtensions
254+
{
255+
public static void SetUser(this Datadog.Trace.ISpan span, Datadog.Trace.UserDetails userDetails) { }
256+
}
253257
public static class SpanKinds
254258
{
255259
public const string Client = "client";
@@ -315,6 +319,15 @@ namespace Datadog.Trace
315319
public Datadog.Trace.IScope StartActive(string operationName, Datadog.Trace.SpanCreationSettings settings) { }
316320
public static void Configure(Datadog.Trace.Configuration.TracerSettings settings) { }
317321
}
322+
public class UserDetails
323+
{
324+
public UserDetails() { }
325+
public string Email { get; set; }
326+
public string Id { get; set; }
327+
public string Name { get; set; }
328+
public string Role { get; set; }
329+
public string SessionId { get; set; }
330+
}
318331
}
319332
namespace Datadog.Trace.ExtensionMethods
320333
{

tracer/test/Datadog.Trace.Tests/TracerTests.cs

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
// </copyright>
55

66
using System;
7+
using System.Collections.Generic;
78
using System.Linq;
89
using System.Net;
910
using System.Threading.Tasks;
@@ -524,5 +525,175 @@ public async Task ForceFlush()
524525

525526
agent.Verify(a => a.FlushTracesAsync(), Times.Once);
526527
}
528+
529+
[Fact]
530+
public void SetUserOnRootSpanDirectly()
531+
{
532+
var scopeManager = new AsyncLocalScopeManager();
533+
534+
var settings = new TracerSettings
535+
{
536+
StartupDiagnosticLogEnabled = false
537+
};
538+
var tracer = new Tracer(settings, Mock.Of<IAgentWriter>(), Mock.Of<ISampler>(), scopeManager, Mock.Of<IDogStatsd>());
539+
540+
var rootTestScope = tracer.StartActive("test.trace");
541+
var childTestScope = tracer.StartActive("test.trace.child");
542+
childTestScope.Dispose();
543+
544+
var email = "test@adventure-works.com";
545+
var name = "Jane Doh";
546+
var id = Guid.NewGuid().ToString();
547+
var sessionId = Guid.NewGuid().ToString();
548+
var role = "admin";
549+
550+
var userDetails = new UserDetails()
551+
{
552+
Email = email,
553+
Name = name,
554+
Id = id,
555+
SessionId = sessionId,
556+
Role = role,
557+
};
558+
tracer.ActiveScope?.Span.SetUser(userDetails);
559+
560+
Assert.Equal(email, rootTestScope.Span.GetTag(Tags.User.Email));
561+
Assert.Equal(name, rootTestScope.Span.GetTag(Tags.User.Name));
562+
Assert.Equal(id, rootTestScope.Span.GetTag(Tags.User.Id));
563+
Assert.Equal(sessionId, rootTestScope.Span.GetTag(Tags.User.SessionId));
564+
Assert.Equal(role, rootTestScope.Span.GetTag(Tags.User.Role));
565+
}
566+
567+
[Fact]
568+
public void SetUserOnChildChildSpan_ShouldAttachToRoot()
569+
{
570+
var scopeManager = new AsyncLocalScopeManager();
571+
572+
var settings = new TracerSettings
573+
{
574+
StartupDiagnosticLogEnabled = false
575+
};
576+
var tracer = new Tracer(settings, Mock.Of<IAgentWriter>(), Mock.Of<ISampler>(), scopeManager, Mock.Of<IDogStatsd>());
577+
578+
var rootTestScope = tracer.StartActive("test.trace");
579+
var childTestScope = tracer.StartActive("test.trace.child");
580+
581+
var email = "test@adventure-works.com";
582+
var name = "Jane Doh";
583+
var id = Guid.NewGuid().ToString();
584+
var sessionId = Guid.NewGuid().ToString();
585+
var role = "admin";
586+
587+
var userDetails = new UserDetails()
588+
{
589+
Email = email,
590+
Name = name,
591+
Id = id,
592+
SessionId = sessionId,
593+
Role = role,
594+
};
595+
tracer.ActiveScope?.Span.SetUser(userDetails);
596+
597+
childTestScope.Dispose();
598+
599+
Assert.Equal(email, rootTestScope.Span.GetTag(Tags.User.Email));
600+
Assert.Equal(name, rootTestScope.Span.GetTag(Tags.User.Name));
601+
Assert.Equal(id, rootTestScope.Span.GetTag(Tags.User.Id));
602+
Assert.Equal(sessionId, rootTestScope.Span.GetTag(Tags.User.SessionId));
603+
Assert.Equal(role, rootTestScope.Span.GetTag(Tags.User.Role));
604+
}
605+
606+
[Fact]
607+
public void SetUser_ShouldWorkOnAnythingImplementingISpan()
608+
{
609+
var testSpan = new SpanStub();
610+
611+
var email = "test@adventure-works.com";
612+
var name = "Jane Doh";
613+
var id = Guid.NewGuid().ToString();
614+
var sessionId = Guid.NewGuid().ToString();
615+
var role = "admin";
616+
617+
var userDetails = new UserDetails()
618+
{
619+
Email = email,
620+
Name = name,
621+
Id = id,
622+
SessionId = sessionId,
623+
Role = role,
624+
};
625+
testSpan.SetUser(userDetails);
626+
627+
Assert.Equal(email, testSpan.GetTag(Tags.User.Email));
628+
Assert.Equal(name, testSpan.GetTag(Tags.User.Name));
629+
Assert.Equal(id, testSpan.GetTag(Tags.User.Id));
630+
Assert.Equal(sessionId, testSpan.GetTag(Tags.User.SessionId));
631+
Assert.Equal(role, testSpan.GetTag(Tags.User.Role));
632+
}
633+
634+
[Fact]
635+
public void SetUser_ShouldThrowAnExceptionIfNoIdIsProvided()
636+
{
637+
var testSpan = new SpanStub();
638+
639+
var email = "test@adventure-works.com";
640+
641+
var userDetails = new UserDetails()
642+
{
643+
Email = email,
644+
};
645+
646+
Assert.ThrowsAny<ArgumentException>(() =>
647+
testSpan.SetUser(userDetails));
648+
}
649+
650+
private class SpanStub : ISpan
651+
{
652+
private Dictionary<string, string> _tags = new Dictionary<string, string>();
653+
654+
public string OperationName { get; set; }
655+
656+
public string ResourceName { get; set; }
657+
658+
public string Type { get; set; }
659+
660+
public bool Error { get; set; }
661+
662+
public string ServiceName { get; set; }
663+
664+
public ulong TraceId => 1ul;
665+
666+
public ulong SpanId => 1ul;
667+
668+
public ISpanContext Context => null;
669+
670+
public void Dispose()
671+
{
672+
}
673+
674+
public void Finish()
675+
{
676+
}
677+
678+
public void Finish(DateTimeOffset finishTimestamp)
679+
{
680+
}
681+
682+
public string GetTag(string key)
683+
{
684+
_tags.TryGetValue(key, out var value);
685+
return value;
686+
}
687+
688+
public void SetException(Exception exception)
689+
{
690+
}
691+
692+
public ISpan SetTag(string key, string value)
693+
{
694+
_tags[key] = value;
695+
return this;
696+
}
697+
}
527698
}
528699
}

0 commit comments

Comments
 (0)