Skip to content

Commit b3dcd02

Browse files
Add setUser to user monitoring SDK
1 parent d0b259b commit b3dcd02

File tree

9 files changed

+144
-26
lines changed

9 files changed

+144
-26
lines changed

dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/user/AppSecEventTrackerSpecification.groovy

+23
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package com.datadog.appsec.user
33
import com.datadog.appsec.gateway.NoopFlow
44
import datadog.appsec.api.blocking.BlockingContentType
55
import datadog.appsec.api.blocking.BlockingException
6+
import datadog.appsec.api.user.User
67
import datadog.trace.api.EventTracker
78
import datadog.trace.api.GlobalTracer
89
import datadog.trace.api.ProductTraceSource
@@ -31,6 +32,7 @@ import static datadog.trace.api.gateway.Events.EVENTS
3132
import static datadog.trace.api.telemetry.LoginEvent.LOGIN_FAILURE
3233
import static datadog.trace.api.telemetry.LoginEvent.LOGIN_SUCCESS
3334
import static datadog.trace.api.telemetry.LoginEvent.SIGN_UP
35+
import static datadog.appsec.api.user.User.setUser
3436

3537
class AppSecEventTrackerSpecification extends DDSpecification {
3638

@@ -72,6 +74,7 @@ class AppSecEventTrackerSpecification extends DDSpecification {
7274
}
7375
}
7476
GlobalTracer.setEventTracker(tracker)
77+
User.setUserService(tracker)
7578
ActiveSubsystems.APPSEC_ACTIVE = true
7679
}
7780

@@ -128,6 +131,20 @@ class AppSecEventTrackerSpecification extends DDSpecification {
128131
0 * _
129132
}
130133
134+
def 'test track user (SDK)'() {
135+
when:
136+
setUser(USER_ID, ['key1': 'value1', 'key2': 'value2'])
137+
138+
then:
139+
1 * traceSegment.setTagTop('usr.id', USER_ID)
140+
1 * traceSegment.setTagTop('usr', ['key1':'value1', 'key2':'value2'])
141+
1 * traceSegment.setTagTop('_dd.appsec.user.collection_mode', SDK.fullName())
142+
1 * traceSegment.setTagTop('asm.keep', true)
143+
1 * traceSegment.setTagTop('_dd.p.ts', ProductTraceSource.ASM)
144+
1 * user.apply(_ as RequestContext, USER_ID) >> NoopFlow.INSTANCE
145+
0 * _
146+
}
147+
131148
def 'test wrong event argument validation (SDK)'() {
132149
when:
133150
GlobalTracer.getEventTracker().trackLoginSuccessEvent(null, null)
@@ -164,6 +181,12 @@ class AppSecEventTrackerSpecification extends DDSpecification {
164181
165182
then:
166183
thrown IllegalArgumentException
184+
185+
when:
186+
setUser(null, null)
187+
188+
then:
189+
thrown IllegalArgumentException
167190
}
168191
169192
def "test onSignup (#mode)"() {

dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/user/EventTrackerAppSecDisabledForkedTest.groovy

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
package com.datadog.appsec.user
22

3-
3+
import datadog.appsec.api.user.User
44
import datadog.trace.api.GlobalTracer
55
import datadog.trace.api.UserIdCollectionMode
66
import datadog.trace.api.appsec.AppSecEventTracker
@@ -26,7 +26,7 @@ class EventTrackerAppSecDisabledForkedTest extends DDSpecification {
2626
void setup() {
2727
tracker = new AppSecEventTracker()
2828
GlobalTracer.setEventTracker(tracker)
29-
29+
User.setUserService(tracker)
3030
traceSegment = Mock(TraceSegment)
3131
final tracer = Stub(AgentTracer.TracerAPI) {
3232
getTraceSegment() >> traceSegment

dd-java-agent/appsec/src/testFixtures/groovy/com/datadog/appsec/AppSecHttpServerTest.groovy

+1-2
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,7 @@ abstract class AppSecHttpServerTest<SERVER> extends WithHttpServer<SERVER> {
2626
sco.createRemaining(config)
2727
assert sco.configurationPoller(config) == null
2828
assert sco.monitoring instanceof Monitoring.DisabledMonitoring
29-
30-
GlobalTracer.setEventTracker(new AppSecEventTracker())
29+
AppSecEventTracker.install()
3130

3231
AppSecSystem.start(ss, sco)
3332
}

dd-java-agent/instrumentation/spring-security-5/src/test/groovy/datadog/trace/instrumentation/springsecurity5/SpringBootBasedTest.groovy

+36-18
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import datadog.trace.api.config.AppSecConfig
1111
import datadog.trace.core.DDSpan
1212
import okhttp3.FormBody
1313
import okhttp3.HttpUrl
14+
import okhttp3.OkHttpClient
1415
import okhttp3.Request
1516
import okhttp3.RequestBody
1617
import org.springframework.boot.SpringApplication
@@ -290,17 +291,7 @@ class SpringBootBasedTest extends AppSecHttpServerTest<ConfigurableApplicationCo
290291
void 'test user event'() {
291292
setup:
292293
def client = clientBuilder().cookieJar(cookieJar()).followRedirects(false).build()
293-
def formBody = new FormBody.Builder()
294-
.add("username", "admin")
295-
.add("password", "admin")
296-
.build()
297-
298-
def loginRequest = request(LOGIN, "POST", formBody).build()
299-
def loginResponse = client.newCall(loginRequest).execute()
300-
assert loginResponse.code() == LOGIN.status
301-
assert loginResponse.body().string() == LOGIN.body
302-
TEST_WRITER.waitForTraces(1)
303-
TEST_WRITER.start() // clear all traces
294+
doLogin(client, 'admin', 'admin')
304295

305296
when:
306297
def request = request(SUCCESS, "GET", null).build()
@@ -322,13 +313,7 @@ class SpringBootBasedTest extends AppSecHttpServerTest<ConfigurableApplicationCo
322313
setup:
323314
def logMessagePrefix = 'Attempt to replace'
324315
def client = clientBuilder().cookieJar(cookieJar()).followRedirects(false).build()
325-
def formBody = new FormBody.Builder()
326-
.add('username', 'admin')
327-
.add('password', 'admin')
328-
.build()
329-
def loginRequest = request(LOGIN, 'POST', formBody).build()
330-
def loginResponse = client.newCall(loginRequest).execute()
331-
assert loginResponse.code() == LOGIN.status
316+
doLogin(client, 'admin', 'admin')
332317

333318
when: 'sdk with different user'
334319
def sdkBody = new FormBody.Builder().add("sdkUser", "sdkUser").build()
@@ -350,4 +335,37 @@ class SpringBootBasedTest extends AppSecHttpServerTest<ConfigurableApplicationCo
350335
event.message.startsWith(logMessagePrefix)
351336
})
352337
}
338+
339+
void 'test automated user tracking and setUser SDK used simultaneously'() {
340+
setup:
341+
def client = clientBuilder().cookieJar(cookieJar()).followRedirects(false).build()
342+
doLogin(client, 'admin', 'admin')
343+
344+
when:
345+
def sdkBody = new FormBody.Builder().add("sdkEvent", "setUser").add("sdkUser", "sdkUser").build()
346+
def sdkRequest = request(SDK, 'POST', sdkBody).build()
347+
final response = client.newCall(sdkRequest).execute()
348+
TEST_WRITER.waitForTraces(1)
349+
def span = TEST_WRITER.flatten().first() as DDSpan
350+
351+
then:
352+
response.code() == SDK.status
353+
response.body().string() == SDK.body
354+
span.getTag('_dd.appsec.usr.id') == 'admin' //
355+
// SDK should take priority over automated login events
356+
span.getTag('usr.id') == 'sdkUser'
357+
span.getTag('_dd.appsec.user.collection_mode') == 'sdk'
358+
}
359+
360+
private void doLogin(final OkHttpClient client, final String username, final String password) {
361+
def formBody = new FormBody.Builder()
362+
.add('username', username)
363+
.add('password', password)
364+
.build()
365+
def loginRequest = request(LOGIN, 'POST', formBody).build()
366+
def loginResponse = client.newCall(loginRequest).execute()
367+
assert loginResponse.code() == LOGIN.status
368+
TEST_WRITER.waitForTraces(1)
369+
TEST_WRITER.start() // clear all traces
370+
}
353371
}

dd-java-agent/instrumentation/spring-security-5/src/test/groovy/datadog/trace/instrumentation/springsecurity5/UserController.groovy

+16-2
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import org.springframework.web.bind.annotation.RequestMapping
1010
import org.springframework.web.bind.annotation.RequestParam
1111
import org.springframework.web.bind.annotation.ResponseBody
1212

13+
import static datadog.appsec.api.user.User.setUser
1314
import static datadog.trace.instrumentation.springsecurity5.TestEndpoint.REGISTER
1415
import static datadog.trace.instrumentation.springsecurity5.TestEndpoint.SUCCESS
1516
import static datadog.trace.instrumentation.springsecurity5.TestEndpoint.SDK
@@ -48,9 +49,22 @@ class UserController {
4849

4950
@PostMapping("/sdk")
5051
@ResponseBody
51-
String sdk(@RequestParam("sdkUser") String sdkUser) {
52+
String sdk(@RequestParam(name = "sdkEvent", defaultValue = "login.success") String event, @RequestParam(name = "sdkUser", required = false) String sdkUser) {
5253
SpringBootBasedTest.controller(SDK) {
53-
GlobalTracer.getEventTracker().trackLoginSuccessEvent(sdkUser, emptyMap())
54+
switch (event) {
55+
case "login.success":
56+
GlobalTracer.getEventTracker().trackLoginSuccessEvent(sdkUser, emptyMap())
57+
break
58+
case "login.failure":
59+
GlobalTracer.getEventTracker().trackLoginFailureEvent(sdkUser, false, emptyMap())
60+
break
61+
case "setUser":
62+
setUser(sdkUser, emptyMap())
63+
break
64+
default:
65+
GlobalTracer.getEventTracker().trackCustomEvent(event, emptyMap())
66+
break
67+
}
5468
return "OK"
5569
}
5670
}

dd-trace-api/build.gradle

+1
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ excludedClassesCoverage += [
3636
'datadog.trace.api.experimental.DataStreamsContextCarrier',
3737
'datadog.trace.api.experimental.DataStreamsContextCarrier.NoOp',
3838
'datadog.appsec.api.blocking.*',
39+
'datadog.appsec.api.user.*',
3940
// Default fallback methods to not break legacy API
4041
'datadog.trace.context.TraceScope',
4142
'datadog.trace.context.NoopTraceScope.NoopContinuation',
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package datadog.appsec.api.user;
2+
3+
import java.util.Map;
4+
5+
public class User {
6+
7+
private static volatile UserService SERVICE = UserService.NO_OP;
8+
9+
/**
10+
* Controls the implementation for user service. The AppSec subsystem calls this method on
11+
* startup. This can be called explicitly for e.g. testing purposes.
12+
*
13+
* @param service the implementation for the user service.
14+
*/
15+
public static void setUserService(final UserService service) {
16+
SERVICE = service;
17+
}
18+
19+
/**
20+
* Sets the user monitoring tags on the root span using the prefix {@code usr}
21+
*
22+
* @param id identifier of the user
23+
* @param metadata custom metadata data represented as key/value map
24+
*/
25+
public static void setUser(final String id, final Map<String, String> metadata) {
26+
SERVICE.trackUserEvent(id, metadata);
27+
}
28+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package datadog.appsec.api.user;
2+
3+
import java.util.Map;
4+
5+
public interface UserService {
6+
7+
UserService NO_OP =
8+
new UserService() {
9+
@Override
10+
public void trackUserEvent(final String userId, final Map<String, String> metadata) {}
11+
};
12+
13+
void trackUserEvent(String userId, Map<String, String> metadata);
14+
}

internal-api/src/main/java/datadog/trace/api/appsec/AppSecEventTracker.java

+23-2
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,11 @@
88
import static datadog.trace.api.telemetry.LoginEvent.LOGIN_SUCCESS;
99
import static datadog.trace.api.telemetry.LoginEvent.SIGN_UP;
1010
import static datadog.trace.util.Strings.toHexString;
11+
import static java.util.Collections.emptyMap;
1112

1213
import datadog.appsec.api.blocking.BlockingException;
14+
import datadog.appsec.api.user.User;
15+
import datadog.appsec.api.user.UserService;
1316
import datadog.trace.api.EventTracker;
1417
import datadog.trace.api.GlobalTracer;
1518
import datadog.trace.api.ProductTraceSource;
@@ -30,7 +33,7 @@
3033
import java.util.Map;
3134
import java.util.function.BiFunction;
3235

33-
public class AppSecEventTracker extends EventTracker {
36+
public class AppSecEventTracker extends EventTracker implements UserService {
3437

3538
private static final int HASH_SIZE_BYTES = 16; // 128 bits
3639
private static final String ANON_PREFIX = "anon_";
@@ -40,7 +43,9 @@ public class AppSecEventTracker extends EventTracker {
4043
private static final String SIGNUP_TAG = "users.signup";
4144

4245
public static void install() {
43-
GlobalTracer.setEventTracker(new AppSecEventTracker());
46+
final AppSecEventTracker tracker = new AppSecEventTracker();
47+
GlobalTracer.setEventTracker(tracker);
48+
User.setUserService(tracker);
4449
}
4550

4651
@Override
@@ -68,6 +73,14 @@ public final void trackCustomEvent(String eventName, Map<String, String> metadat
6873
onCustomEvent(SDK, eventName, metadata);
6974
}
7075

76+
@Override
77+
public void trackUserEvent(final String userId, final Map<String, String> metadata) {
78+
if (userId == null || userId.isEmpty()) {
79+
throw new IllegalArgumentException("UserId is null or empty");
80+
}
81+
onUserEvent(SDK, userId, metadata);
82+
}
83+
7184
public void onUserNotFound(final UserIdCollectionMode mode) {
7285
if (!isEnabled(mode)) {
7386
return;
@@ -88,6 +101,11 @@ public void onUserNotFound(final UserIdCollectionMode mode) {
88101
}
89102

90103
public void onUserEvent(final UserIdCollectionMode mode, final String userId) {
104+
onUserEvent(mode, userId, emptyMap());
105+
}
106+
107+
public void onUserEvent(
108+
final UserIdCollectionMode mode, final String userId, final Map<String, String> metadata) {
91109
if (!isEnabled(mode)) {
92110
return;
93111
}
@@ -108,6 +126,9 @@ public void onUserEvent(final UserIdCollectionMode mode, final String userId) {
108126
}
109127
if (isNewUser(mode, segment)) {
110128
segment.setTagTop("usr.id", finalUserId);
129+
if (metadata != null && !metadata.isEmpty()) {
130+
segment.setTagTop("usr", metadata);
131+
}
111132
segment.setTagTop("_dd.appsec.user.collection_mode", mode.fullName());
112133
segment.setTagTop(Tags.ASM_KEEP, true);
113134
segment.setTagTop(Tags.PROPAGATED_TRACE_SOURCE, ProductTraceSource.ASM);

0 commit comments

Comments
 (0)