Skip to content

Commit 59db91a

Browse files
committed
wip
1 parent 75d66b9 commit 59db91a

File tree

2 files changed

+64
-8
lines changed

2 files changed

+64
-8
lines changed

dd-java-agent/appsec/src/main/java/com/datadog/appsec/api/security/ApiSecurityRequestSampler.java

+12-8
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package com.datadog.appsec.api.security;
22

33
import com.datadog.appsec.gateway.AppSecRequestContext;
4+
import datadog.trace.api.time.SystemTimeSource;
5+
import datadog.trace.api.time.TimeSource;
46
import datadog.trace.util.NonBlockingSemaphore;
57

68
import javax.annotation.Nonnull;
@@ -23,18 +25,20 @@ public class ApiSecurityRequestSampler {
2325
private final Deque<Long> apiAccessQueue; // hashes ordered by access time
2426
private final long expirationTimeInMs;
2527
private final int capacity;
28+
private final TimeSource timeSource;
2629

2730
final NonBlockingSemaphore counter = NonBlockingSemaphore.withPermitCount(MAX_POST_PROCESSING_TASKS);
2831

2932
public ApiSecurityRequestSampler() {
30-
this(MAX_SIZE, INTERVAL_SECONDS * 1000);
33+
this(MAX_SIZE, INTERVAL_SECONDS * 1000, SystemTimeSource.INSTANCE);
3134
}
3235

33-
public ApiSecurityRequestSampler(int capacity, long expirationTimeInMs) {
36+
public ApiSecurityRequestSampler(int capacity, long expirationTimeInMs, @Nonnull TimeSource timeSource) {
3437
this.capacity = capacity;
3538
this.expirationTimeInMs = expirationTimeInMs;
3639
this.apiAccessMap = new ConcurrentHashMap<>(MAX_SIZE);
3740
this.apiAccessQueue = new ConcurrentLinkedDeque<>();
41+
this.timeSource = timeSource;
3842
}
3943

4044
public void preSampleRequest(final @Nonnull AppSecRequestContext ctx) {
@@ -79,12 +83,12 @@ public boolean sampleRequest(AppSecRequestContext ctx) {
7983
* synchronization for updating data structures is not required.
8084
*/
8185
public boolean updateApiAccessIfExpired(final long hash) {
82-
final long currentTime = System.currentTimeMillis();
86+
final long currentTime = timeSource.getCurrentTimeMillis();
8387

8488
// New or updated record
8589
boolean isNewOrUpdated = false;
8690
if (!apiAccessMap.containsKey(hash)
87-
|| currentTime - apiAccessMap.get(hash) > expirationTimeInMs) {
91+
|| currentTime - apiAccessMap.get(hash) >= expirationTimeInMs) {
8892

8993
cleanupExpiredEntries(currentTime);
9094

@@ -107,9 +111,9 @@ public boolean updateApiAccessIfExpired(final long hash) {
107111
}
108112

109113
public boolean isApiAccessExpired(final long hash) {
110-
long currentTime = System.currentTimeMillis();
114+
long currentTime = timeSource.getCurrentTimeMillis();
111115
return !apiAccessMap.containsKey(hash)
112-
|| currentTime - apiAccessMap.get(hash) > expirationTimeInMs;
116+
|| currentTime - apiAccessMap.get(hash) >= expirationTimeInMs;
113117
}
114118

115119
private void cleanupExpiredEntries(final long currentTime) {
@@ -118,7 +122,7 @@ private void cleanupExpiredEntries(final long currentTime) {
118122
if (oldestHash == null) break;
119123

120124
Long lastAccessTime = apiAccessMap.get(oldestHash);
121-
if (lastAccessTime == null || currentTime - lastAccessTime > expirationTimeInMs) {
125+
if (lastAccessTime == null || currentTime - lastAccessTime >= expirationTimeInMs) {
122126
apiAccessQueue.pollFirst(); // remove from head
123127
apiAccessMap.remove(oldestHash);
124128
} else {
@@ -137,7 +141,7 @@ private long computeApiHash(final String route, final String method, final int s
137141

138142
public static final class NoOp extends ApiSecurityRequestSampler {
139143
public NoOp() {
140-
super(0, 0);
144+
super(0, 0, SystemTimeSource.INSTANCE);
141145
}
142146

143147
@Override

dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/api/security/ApiSecurityRequestSamplerTest.groovy

+52
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
package com.datadog.appsec.api.security
22

33
import com.datadog.appsec.gateway.AppSecRequestContext
4+
import datadog.trace.api.time.ControllableTimeSource
45
import datadog.trace.test.util.DDSpecification
56

7+
import java.time.Duration
8+
69
class ApiSecurityRequestSamplerTest extends DDSpecification {
710

811
void 'happy path with single request'() {
@@ -149,6 +152,55 @@ class ApiSecurityRequestSamplerTest extends DDSpecification {
149152
0 * _
150153
}
151154

155+
void 'sampleRequest with null context'() {
156+
given:
157+
def sampler = new ApiSecurityRequestSampler()
158+
159+
when:
160+
def sampleDecision = sampler.sampleRequest(null)
161+
162+
then:
163+
!sampleDecision
164+
}
165+
166+
void 'sampleRequest honors expiration'() {
167+
given:
168+
def ctx = createContext('route1', 'GET', 200)
169+
ctx.setApiSecurityEndpointHash(42L)
170+
ctx.setKeepOpenForApiSecurityPostProcessing(true)
171+
ctx = Spy(ctx)
172+
final timeSource = new ControllableTimeSource()
173+
timeSource.set(0)
174+
final long expirationTimeInMs = 10L
175+
final long expirationTimeInNs = expirationTimeInMs * 1_000_000
176+
def sampler = new ApiSecurityRequestSampler(10, expirationTimeInMs, timeSource)
177+
178+
when:
179+
def sampleDecision = sampler.sampleRequest(ctx)
180+
181+
then:
182+
sampleDecision
183+
1 * ctx.getApiSecurityEndpointHash()
184+
0 * _
185+
186+
when:
187+
sampleDecision = sampler.sampleRequest(ctx)
188+
189+
then: 'second request is not sampled'
190+
!sampleDecision
191+
1 * ctx.getApiSecurityEndpointHash()
192+
0 * _
193+
194+
when: 'expiration time has passed'
195+
timeSource.advance(expirationTimeInNs)
196+
sampleDecision = sampler.sampleRequest(ctx)
197+
198+
then: 'request is sampled again'
199+
sampleDecision
200+
1 * ctx.getApiSecurityEndpointHash()
201+
0 * _
202+
}
203+
152204
private AppSecRequestContext createContext(final String route, final String method, int statusCode) {
153205
final AppSecRequestContext ctx = new AppSecRequestContext()
154206
ctx.setRoute(route)

0 commit comments

Comments
 (0)