Skip to content

Commit c5fbd0b

Browse files
Nithin-Kasamkingzacko1danotorrey
authored
Added Secondary token support for Raw Http input (#25544)
* implemented secondary token * Changelog cleanup * Change log nit, test enhancement --------- Co-authored-by: Zack King <zack.king@graylog.com> Co-authored-by: Dan Torrey <dan.torrey@graylog.com>
1 parent f25597c commit c5fbd0b

File tree

7 files changed

+104
-40
lines changed

7 files changed

+104
-40
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
type = "a"
2+
message = "Add secondary authorization token support to HTTP-based inputs."
3+
4+
issues = ["Graylog2/graylog-plugin-enterprise#13702"]
5+
pulls = ["25544"]

graylog2-server/src/main/java/org/graylog/inputs/otel/transport/OTelHttpHandler.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import org.slf4j.LoggerFactory;
3434

3535
import java.net.InetSocketAddress;
36+
import java.util.Set;
3637
import java.util.function.Function;
3738
import java.util.stream.Stream;
3839

@@ -58,9 +59,9 @@ public class OTelHttpHandler extends HttpHandler {
5859
private final MessageInput input;
5960

6061
public OTelHttpHandler(boolean enableCors, String authorizationHeader,
61-
String authorizationHeaderValue, String path,
62+
Set<String> authorizationHeaderValues, String path,
6263
MessageInput input) {
63-
super(enableCors, authorizationHeader, authorizationHeaderValue, path);
64+
super(enableCors, authorizationHeader, authorizationHeaderValues, path);
6465
this.input = input;
6566
}
6667

@@ -119,7 +120,7 @@ protected void handleValidPost(ChannelHandlerContext ctx, FullHttpRequest reques
119120
* Sends an OTLP-conformant error response in the encoding matching the request.
120121
*/
121122
private void sendOtlpError(ChannelHandlerContext ctx, FullHttpRequest request, boolean keepAlive,
122-
String origin, boolean protobuf, HttpResponseStatus status) {
123+
String origin, boolean protobuf, HttpResponseStatus status) {
123124
final byte[] body = OtlpHttpUtils.buildErrorStatus(status, null, protobuf);
124125
final String contentType = protobuf ? OtlpHttpUtils.PROTOBUF_CONTENT_TYPE : OtlpHttpUtils.JSON_CONTENT_TYPE;
125126
writeResponse(ctx.channel(), keepAlive, request.protocolVersion(), status, origin, body, contentType);

graylog2-server/src/main/java/org/graylog/inputs/otel/transport/OTelHttpTransport.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ protected LinkedHashMap<String, Callable<? extends ChannelHandler>> getCustomChi
7575
// input.processRawMessage directly. These cannot be removed without overriding
7676
// getChildChannelHandlers.
7777
handlers.replace("http-handler", () -> new OTelHttpHandler(
78-
isEnableCors(), getAuthorizationHeader(), getAuthorizationHeaderValue(),
78+
isEnableCors(), getAuthorizationHeader(), getAuthorizationHeaderValues(),
7979
getPath(), input));
8080
return handlers;
8181
}

graylog2-server/src/main/java/org/graylog2/inputs/transports/AbstractHttpTransport.java

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@
5252
import java.util.Set;
5353
import java.util.concurrent.Callable;
5454
import java.util.concurrent.TimeUnit;
55+
import java.util.stream.Collectors;
56+
import java.util.stream.Stream;
5557

5658
import static org.apache.commons.lang3.StringUtils.isBlank;
5759
import static org.apache.commons.lang3.StringUtils.isNotBlank;
@@ -71,6 +73,8 @@ abstract public class AbstractHttpTransport extends AbstractTcpTransport {
7173
static final String CK_AUTHORIZATION_HEADER_VALUE = "authorization_header_value";
7274
private static final String AUTHORIZATION_HEADER_NAME_LABEL = "Authorization Header Name";
7375
private static final String AUTHORIZATION_HEADER_VALUE_LABEL = "Authorization Header Value";
76+
private static final String AUTHORIZATION_HEADER_VALUE_SECONDARY_LABEL = "Authorization Header Value (secondary)";
77+
private static final String CK_AUTHORIZATION_HEADER_VALUE_SECONDARY = "authorization_header_value_secondary";
7478
static final String CK_REAL_IP_HEADER_NAME = "real_ip_header_name";
7579
static final String CK_ENABLE_FORWARDED_FOR = "enable_forwarded_for";
7680
static final String CK_REQUIRE_TRUSTED_PROXIES = "require_trusted_proxies";
@@ -81,7 +85,7 @@ abstract public class AbstractHttpTransport extends AbstractTcpTransport {
8185
protected final int maxChunkSize;
8286
private final int idleWriterTimeout;
8387
private final String authorizationHeader;
84-
private final String authorizationHeaderValue;
88+
private final Set<String> authorizationHeaderValues;
8589
private final Set<IpSubnet> trustedProxies;
8690
private final String path;
8791
private final boolean enableForwardedFor;
@@ -112,7 +116,11 @@ public AbstractHttpTransport(Configuration configuration,
112116
? configuration.getInt(CK_IDLE_WRITER_TIMEOUT, DEFAULT_IDLE_WRITER_TIMEOUT)
113117
: DEFAULT_IDLE_WRITER_TIMEOUT;
114118
this.authorizationHeader = configuration.getString(CK_AUTHORIZATION_HEADER_NAME);
115-
this.authorizationHeaderValue = configuration.getString(CK_AUTHORIZATION_HEADER_VALUE);
119+
this.authorizationHeaderValues = Stream.of(
120+
configuration.getString(CK_AUTHORIZATION_HEADER_VALUE),
121+
configuration.getString(CK_AUTHORIZATION_HEADER_VALUE_SECONDARY)
122+
).filter(v -> v != null && !v.isBlank())
123+
.collect(Collectors.toUnmodifiableSet());
116124
this.enableForwardedFor = configuration.getBoolean(CK_ENABLE_FORWARDED_FOR);
117125
this.requireTrustedProxies = configuration.getBoolean(CK_REQUIRE_TRUSTED_PROXIES);
118126
this.enableRealIpHeader = configuration.getBoolean(CK_ENABLE_REAL_IP_HEADER);
@@ -138,8 +146,8 @@ protected String getAuthorizationHeader() {
138146
return authorizationHeader;
139147
}
140148

141-
protected String getAuthorizationHeaderValue() {
142-
return authorizationHeaderValue;
149+
protected Set<String> getAuthorizationHeaderValues() {
150+
return authorizationHeaderValues;
143151
}
144152

145153
protected String getPath() {
@@ -168,7 +176,7 @@ protected LinkedHashMap<String, Callable<? extends ChannelHandler>> getCustomChi
168176
handlers.put("http-forwarded-for-handler", () -> new HttpForwardedForHandler(enableForwardedFor,
169177
enableRealIpHeader, realIpHeaders, requireTrustedProxies, trustedProxies));
170178
handlers.put("http-handler",
171-
() -> new HttpHandler(enableCors, authorizationHeader, authorizationHeaderValue, path));
179+
() -> new HttpHandler(enableCors, authorizationHeader, authorizationHeaderValues, path));
172180
if (enableBulkReceiving) {
173181
handlers.put("http-bulk-newline-decoder",
174182
() -> new LenientDelimiterBasedFrameDecoder(maxChunkSize,
@@ -183,11 +191,13 @@ protected LinkedHashMap<String, Callable<? extends ChannelHandler>> getCustomChi
183191
@Override
184192
public void launch(MessageInput input, @Nullable InputFailureRecorder inputFailureRecorder)
185193
throws MisfireException {
186-
if (isNotBlank(authorizationHeader) && isBlank(authorizationHeaderValue)) {
187-
checkForConfigFieldDependencies(AUTHORIZATION_HEADER_NAME_LABEL,
194+
if (isNotBlank(authorizationHeader) && authorizationHeaderValues.isEmpty()) {
195+
checkForConfigFieldDependencies(
196+
AUTHORIZATION_HEADER_NAME_LABEL,
188197
AUTHORIZATION_HEADER_VALUE_LABEL);
189-
} else if (isNotBlank(authorizationHeaderValue) && isBlank(authorizationHeader)) {
190-
checkForConfigFieldDependencies(AUTHORIZATION_HEADER_VALUE_LABEL,
198+
} else if (!authorizationHeaderValues.isEmpty() && isBlank(authorizationHeader)) {
199+
checkForConfigFieldDependencies(
200+
AUTHORIZATION_HEADER_VALUE_LABEL,
191201
AUTHORIZATION_HEADER_NAME_LABEL);
192202
}
193203
super.launch(input, inputFailureRecorder);
@@ -236,6 +246,13 @@ public ConfigurationRequest getRequestedConfiguration() {
236246
"The secret authorization header value which all request must have in order to authenticate successfully. e.g. Bearer: <api-token>N",
237247
ConfigurationField.Optional.OPTIONAL,
238248
TextField.Attribute.IS_PASSWORD));
249+
r.addField(new TextField(
250+
CK_AUTHORIZATION_HEADER_VALUE_SECONDARY,
251+
AUTHORIZATION_HEADER_VALUE_SECONDARY_LABEL,
252+
"",
253+
"Optional secondary authorization header value to accept during token rotation. Remove once all clients have migrated to the new token.",
254+
ConfigurationField.Optional.OPTIONAL,
255+
TextField.Attribute.IS_PASSWORD));
239256
r.addField(new BooleanField(
240257
CK_ENABLE_FORWARDED_FOR,
241258
"Take original client IP from X-Forwarded-For or Forwarded headers",

graylog2-server/src/main/java/org/graylog2/inputs/transports/netty/HttpHandler.java

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@
3232
import io.netty.handler.codec.http.HttpVersion;
3333
import jakarta.annotation.Nullable;
3434

35+
import java.util.Set;
36+
3537
import static org.apache.commons.lang.StringUtils.isBlank;
3638
import static org.apache.commons.lang3.StringUtils.isNotBlank;
3739

@@ -40,13 +42,13 @@
4042
public class HttpHandler extends SimpleChannelInboundHandler<HttpRequest> {
4143
private final boolean enableCors;
4244
private final String authorizationHeader;
43-
private final String authorizationHeaderValue;
45+
private final Set<String> authorizationHeaderValues;
4446
private final String path;
4547

46-
public HttpHandler(boolean enableCors, String authorizationHeader, String authorizationHeaderValue, String path) {
48+
public HttpHandler(boolean enableCors, String authorizationHeader, Set<String> authorizationHeaderValues, String path) {
4749
this.enableCors = enableCors;
4850
this.authorizationHeader = authorizationHeader;
49-
this.authorizationHeaderValue = authorizationHeaderValue;
51+
this.authorizationHeaderValues = authorizationHeaderValues;
5052
this.path = path;
5153
}
5254

@@ -65,7 +67,7 @@ protected void channelRead0(ChannelHandlerContext ctx, HttpRequest request) thro
6567

6668
if (isNotBlank(authorizationHeader)) {
6769
final String suppliedAuthHeaderValue = request.headers().get(authorizationHeader);
68-
if (isBlank(suppliedAuthHeaderValue) || !suppliedAuthHeaderValue.equals(authorizationHeaderValue)) {
70+
if (isBlank(suppliedAuthHeaderValue) || !authorizationHeaderValues.contains(suppliedAuthHeaderValue)) {
6971
writeResponse(channel, keepAlive, httpRequestVersion, HttpResponseStatus.UNAUTHORIZED, origin);
7072
return;
7173
}

graylog2-server/src/test/java/org/graylog/inputs/otel/transport/OTelHttpHandlerTest.java

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@
4646
import java.net.InetAddress;
4747
import java.net.InetSocketAddress;
4848
import java.nio.charset.StandardCharsets;
49+
import java.util.Set;
50+
import java.util.stream.Collectors;
51+
import java.util.stream.Stream;
4952

5053
import static org.assertj.core.api.Assertions.assertThat;
5154
import static org.mockito.ArgumentMatchers.any;
@@ -229,7 +232,7 @@ void invalidJsonReturns400WithJsonContentType() {
229232

230233
@Test
231234
void requestWithValidAuthReturns200() throws Exception {
232-
final EmbeddedChannel channel = createChannel(false, "Authorization", "Bearer secret");
235+
final EmbeddedChannel channel = createChannel(false, "Authorization", "Bearer secret", null);
233236
final ExportLogsServiceRequest request = createTestRequest();
234237

235238
final FullHttpRequest httpRequest = new DefaultFullHttpRequest(
@@ -247,9 +250,29 @@ void requestWithValidAuthReturns200() throws Exception {
247250
response.release();
248251
}
249252

253+
@Test
254+
void requestWithValidSecondaryAuthReturns200() throws Exception {
255+
final EmbeddedChannel channel = createChannel(false, "Authorization", "Bearer primary", "Bearer secondary");
256+
final ExportLogsServiceRequest request = createTestRequest();
257+
258+
final FullHttpRequest httpRequest = new DefaultFullHttpRequest(
259+
HttpVersion.HTTP_1_1, HttpMethod.POST, "/v1/logs",
260+
Unpooled.wrappedBuffer(request.toByteArray()));
261+
httpRequest.headers().set(HttpHeaderNames.CONTENT_TYPE, "application/x-protobuf");
262+
httpRequest.headers().set(HttpHeaderNames.CONTENT_LENGTH, request.toByteArray().length);
263+
httpRequest.headers().set("Authorization", "Bearer secondary");
264+
265+
channel.writeInbound(httpRequest);
266+
267+
final FullHttpResponse response = channel.readOutbound();
268+
assertThat(response.status()).isEqualTo(HttpResponseStatus.OK);
269+
verify(input).processRawMessage(any());
270+
response.release();
271+
}
272+
250273
@Test
251274
void requestWithBadAuthReturns401() {
252-
final EmbeddedChannel channel = createChannel(false, "Authorization", "Bearer secret");
275+
final EmbeddedChannel channel = createChannel(false, "Authorization", "Bearer secret", null);
253276

254277
final FullHttpRequest httpRequest = new DefaultFullHttpRequest(
255278
HttpVersion.HTTP_1_1, HttpMethod.POST, "/v1/logs",
@@ -268,7 +291,7 @@ void requestWithBadAuthReturns401() {
268291

269292
@Test
270293
void requestWithMissingAuthReturns401() {
271-
final EmbeddedChannel channel = createChannel(false, "Authorization", "Bearer secret");
294+
final EmbeddedChannel channel = createChannel(false, "Authorization", "Bearer secret", null);
272295

273296
final FullHttpRequest httpRequest = new DefaultFullHttpRequest(
274297
HttpVersion.HTTP_1_1, HttpMethod.POST, "/v1/logs",
@@ -286,7 +309,7 @@ void requestWithMissingAuthReturns401() {
286309

287310
@Test
288311
void optionsPreflightSucceedsEvenWithAuthConfigured() {
289-
final EmbeddedChannel channel = createChannel(true, "Authorization", "Bearer secret");
312+
final EmbeddedChannel channel = createChannel(true, "Authorization", "Bearer secret", null);
290313

291314
final FullHttpRequest httpRequest = new DefaultFullHttpRequest(
292315
HttpVersion.HTTP_1_1, HttpMethod.OPTIONS, "/v1/logs");
@@ -306,7 +329,7 @@ void optionsPreflightSucceedsEvenWithAuthConfigured() {
306329

307330
@Test
308331
void optionsRequestReturns200() {
309-
final EmbeddedChannel channel = createChannel(true, null, null);
332+
final EmbeddedChannel channel = createChannel(true, null, null, null);
310333

311334
final FullHttpRequest httpRequest = new DefaultFullHttpRequest(
312335
HttpVersion.HTTP_1_1, HttpMethod.OPTIONS, "/v1/logs");
@@ -325,7 +348,7 @@ void optionsRequestReturns200() {
325348

326349
@Test
327350
void corsHeadersOnSuccessResponse() throws Exception {
328-
final EmbeddedChannel channel = createChannel(true, null, null);
351+
final EmbeddedChannel channel = createChannel(true, null, null, null);
329352
final ExportLogsServiceRequest request = createTestRequest();
330353

331354
final FullHttpRequest httpRequest = new DefaultFullHttpRequest(
@@ -390,12 +413,16 @@ void processingFailureReturns500WithMatchingContentType() {
390413
}
391414

392415
private EmbeddedChannel createChannel() {
393-
return createChannel(false, null, null);
416+
return createChannel(false, null, null, null);
394417
}
395418

396-
private EmbeddedChannel createChannel(boolean enableCors, String authHeader, String authHeaderValue) {
397-
return new EmbeddedChannel(new OTelHttpHandler(enableCors, authHeader, authHeaderValue,
398-
OTelHttpHandler.LOGS_PATH, input));
419+
private EmbeddedChannel createChannel(boolean enableCors, String authHeader,
420+
String primaryValue, String secondaryValue) {
421+
final Set<String> authorizationHeaderValues = Stream.of(primaryValue, secondaryValue)
422+
.filter(v -> v != null && !v.isBlank())
423+
.collect(Collectors.toUnmodifiableSet());
424+
return new EmbeddedChannel(new OTelHttpHandler(enableCors, authHeader,
425+
authorizationHeaderValues, OTelHttpHandler.LOGS_PATH, input));
399426
}
400427

401428
private ExportLogsServiceRequest createTestRequest() {

0 commit comments

Comments
 (0)