Skip to content

Commit 46e5937

Browse files
Implementing basic authentication for webhook auditlog sink (#5792)
Signed-off-by: Sander van de Geijn <[email protected]>
1 parent c6b6ead commit 46e5937

File tree

8 files changed

+206
-22
lines changed

8 files changed

+206
-22
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
55

66
## [Unreleased 3.x]
77
### Added
8+
- Add support for Basic Authentication in webhook audit log sink using `plugins.security.audit.config.username` and `plugins.security.audit.config.password` ([#5792](https://github.com/opensearch-project/security/pull/5792))
89

910
### Changed
1011
- Ensure all restHeaders from ActionPlugin.getRestHeaders are carried to threadContext for tracing ([#5396](https://github.com/opensearch-project/security/pull/5396))

src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1827,14 +1827,14 @@ public List<Setting<?>> getSettings() {
18271827
); // not filtered here
18281828
settings.add(
18291829
Setting.simpleString(
1830-
ConfigConstants.SECURITY_AUDIT_CONFIG_DEFAULT_PREFIX + ConfigConstants.SECURITY_AUDIT_EXTERNAL_OPENSEARCH_USERNAME,
1830+
ConfigConstants.SECURITY_AUDIT_CONFIG_DEFAULT_PREFIX + ConfigConstants.SECURITY_AUDIT_CONFIG_USERNAME,
18311831
Property.NodeScope,
18321832
Property.Filtered
18331833
)
18341834
);
18351835
settings.add(
18361836
Setting.simpleString(
1837-
ConfigConstants.SECURITY_AUDIT_CONFIG_DEFAULT_PREFIX + ConfigConstants.SECURITY_AUDIT_EXTERNAL_OPENSEARCH_PASSWORD,
1837+
ConfigConstants.SECURITY_AUDIT_CONFIG_DEFAULT_PREFIX + ConfigConstants.SECURITY_AUDIT_CONFIG_PASSWORD,
18381838
Property.NodeScope,
18391839
Property.Filtered
18401840
)

src/main/java/org/opensearch/security/auditlog/sink/ExternalOpenSearchSink.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,8 +83,8 @@ public ExternalOpenSearchSink(
8383
ConfigConstants.SECURITY_AUDIT_EXTERNAL_OPENSEARCH_ENABLE_SSL_CLIENT_AUTH,
8484
ConfigConstants.OPENDISTRO_SECURITY_AUDIT_SSL_ENABLE_SSL_CLIENT_AUTH_DEFAULT
8585
);
86-
final String user = sinkSettings.get(ConfigConstants.SECURITY_AUDIT_EXTERNAL_OPENSEARCH_USERNAME);
87-
final String password = sinkSettings.get(ConfigConstants.SECURITY_AUDIT_EXTERNAL_OPENSEARCH_PASSWORD);
86+
final String user = sinkSettings.get(ConfigConstants.SECURITY_AUDIT_CONFIG_USERNAME);
87+
final String password = sinkSettings.get(ConfigConstants.SECURITY_AUDIT_CONFIG_PASSWORD);
8888

8989
final HttpClientBuilder builder = HttpClient.builder(servers.toArray(new String[0]));
9090

src/main/java/org/opensearch/security/auditlog/sink/WebhookSink.java

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import java.nio.file.Path;
1919
import java.security.KeyStore;
2020
import java.security.cert.X509Certificate;
21+
import java.util.Base64;
2122
import java.util.concurrent.TimeUnit;
2223
import javax.net.ssl.SSLContext;
2324

@@ -60,6 +61,9 @@ public class WebhookSink extends AuditLogSink {
6061
WebhookFormat webhookFormat = null;
6162
final boolean verifySSL;
6263
final KeyStore effectiveTruststore;
64+
private final String username;
65+
private final String password;
66+
private final String basicAuthHeader;
6367

6468
public WebhookSink(
6569
final String name,
@@ -77,6 +81,19 @@ public WebhookSink(
7781
final String webhookUrl = sinkSettings.get(ConfigConstants.SECURITY_AUDIT_WEBHOOK_URL);
7882
final String format = sinkSettings.get(ConfigConstants.SECURITY_AUDIT_WEBHOOK_FORMAT);
7983

84+
// Read basic auth credentials
85+
this.username = sinkSettings.get(ConfigConstants.SECURITY_AUDIT_CONFIG_USERNAME);
86+
this.password = sinkSettings.get(ConfigConstants.SECURITY_AUDIT_CONFIG_PASSWORD);
87+
88+
// Generate Basic Auth header if credentials are provided
89+
if (this.username != null && this.password != null) {
90+
String credentials = this.username + ":" + this.password;
91+
String encodedCredentials = Base64.getEncoder().encodeToString(credentials.getBytes(StandardCharsets.UTF_8));
92+
this.basicAuthHeader = "Basic " + encodedCredentials;
93+
} else {
94+
this.basicAuthHeader = null;
95+
}
96+
8097
verifySSL = sinkSettings.getAsBoolean(ConfigConstants.SECURITY_AUDIT_WEBHOOK_SSL_VERIFY, true);
8198
httpClient = getHttpClient();
8299

@@ -225,6 +242,12 @@ boolean get(AuditMessage msg) {
225242

226243
protected boolean doGet(String url) {
227244
HttpGet httpGet = new HttpGet(url);
245+
246+
// Add Basic Auth header if credentials are configured
247+
if (basicAuthHeader != null) {
248+
httpGet.setHeader("Authorization", basicAuthHeader);
249+
}
250+
228251
CloseableHttpResponse serverResponse = null;
229252
try {
230253
serverResponse = httpClient.execute(httpGet);
@@ -280,6 +303,11 @@ protected boolean doPost(String url, String payload) {
280303

281304
HttpPost postRequest = new HttpPost(url);
282305

306+
// Add Basic Auth header if credentials are configured
307+
if (basicAuthHeader != null) {
308+
postRequest.setHeader("Authorization", basicAuthHeader);
309+
}
310+
283311
StringEntity input = new StringEntity(payload, webhookFormat.contentType.withCharset(StandardCharsets.UTF_8));
284312
postRequest.setEntity(input);
285313

src/main/java/org/opensearch/security/support/ConfigConstants.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -243,8 +243,8 @@ public class ConfigConstants {
243243

244244
// External OpenSearch
245245
public static final String SECURITY_AUDIT_EXTERNAL_OPENSEARCH_HTTP_ENDPOINTS = "http_endpoints";
246-
public static final String SECURITY_AUDIT_EXTERNAL_OPENSEARCH_USERNAME = "username";
247-
public static final String SECURITY_AUDIT_EXTERNAL_OPENSEARCH_PASSWORD = "password";
246+
public static final String SECURITY_AUDIT_CONFIG_USERNAME = "username";
247+
public static final String SECURITY_AUDIT_CONFIG_PASSWORD = "password";
248248
public static final String SECURITY_AUDIT_EXTERNAL_OPENSEARCH_ENABLE_SSL = "enable_ssl";
249249
public static final String SECURITY_AUDIT_EXTERNAL_OPENSEARCH_VERIFY_HOSTNAMES = "verify_hostnames";
250250
public static final String SECURITY_AUDIT_EXTERNAL_OPENSEARCH_ENABLE_SSL_CLIENT_AUTH = "enable_ssl_client_auth";

src/test/java/org/opensearch/security/auditlog/helper/TestHttpHandler.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,12 @@
1313

1414
import java.io.IOException;
1515
import java.nio.charset.StandardCharsets;
16+
import java.util.HashMap;
17+
import java.util.Map;
1618

1719
import org.apache.hc.core5.http.ClassicHttpRequest;
1820
import org.apache.hc.core5.http.ClassicHttpResponse;
21+
import org.apache.hc.core5.http.Header;
1922
import org.apache.hc.core5.http.HttpEntity;
2023
import org.apache.hc.core5.http.HttpException;
2124
import org.apache.hc.core5.http.io.HttpRequestHandler;
@@ -26,12 +29,19 @@ public class TestHttpHandler implements HttpRequestHandler {
2629
public String method;
2730
public String uri;
2831
public String body;
32+
public Map<String, String> headers;
2933

3034
@Override
3135
public void handle(ClassicHttpRequest request, ClassicHttpResponse response, HttpContext context) throws HttpException, IOException {
3236
this.method = request.getMethod();
3337
this.uri = request.getRequestUri();
3438

39+
// Capture headers
40+
this.headers = new HashMap<>();
41+
for (Header header : request.getHeaders()) {
42+
this.headers.put(header.getName(), header.getValue());
43+
}
44+
3545
HttpEntity entity = request.getEntity();
3646
body = EntityUtils.toString(entity, StandardCharsets.UTF_8);
3747
}
@@ -40,5 +50,6 @@ public void reset() {
4050
this.body = null;
4151
this.uri = null;
4252
this.method = null;
53+
this.headers = null;
4354
}
4455
}

src/test/java/org/opensearch/security/auditlog/integration/SSLAuditlogTest.java

Lines changed: 4 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -91,14 +91,8 @@ public void testExternalPemUserPass() throws Exception {
9191
ConfigConstants.SECURITY_AUDIT_CONFIG_DEFAULT_PREFIX + ConfigConstants.SECURITY_AUDIT_EXTERNAL_OPENSEARCH_PEMKEY_FILEPATH,
9292
FileHelper.getAbsoluteFilePathFromClassPath("auditlog/spock.key.pem")
9393
)
94-
.put(
95-
ConfigConstants.SECURITY_AUDIT_CONFIG_DEFAULT_PREFIX + ConfigConstants.SECURITY_AUDIT_EXTERNAL_OPENSEARCH_USERNAME,
96-
"admin"
97-
)
98-
.put(
99-
ConfigConstants.SECURITY_AUDIT_CONFIG_DEFAULT_PREFIX + ConfigConstants.SECURITY_AUDIT_EXTERNAL_OPENSEARCH_PASSWORD,
100-
"admin"
101-
)
94+
.put(ConfigConstants.SECURITY_AUDIT_CONFIG_DEFAULT_PREFIX + ConfigConstants.SECURITY_AUDIT_CONFIG_USERNAME, "admin")
95+
.put(ConfigConstants.SECURITY_AUDIT_CONFIG_DEFAULT_PREFIX + ConfigConstants.SECURITY_AUDIT_CONFIG_PASSWORD, "admin")
10296
.build();
10397

10498
setup(additionalSettings);
@@ -174,14 +168,8 @@ public void testExternalPemUserPassTp() throws Exception {
174168
+ ConfigConstants.SECURITY_AUDIT_EXTERNAL_OPENSEARCH_PEMTRUSTEDCAS_FILEPATH,
175169
FileHelper.getAbsoluteFilePathFromClassPath("auditlog/chain-ca.pem")
176170
)
177-
.put(
178-
ConfigConstants.SECURITY_AUDIT_CONFIG_DEFAULT_PREFIX + ConfigConstants.SECURITY_AUDIT_EXTERNAL_OPENSEARCH_USERNAME,
179-
"admin"
180-
)
181-
.put(
182-
ConfigConstants.SECURITY_AUDIT_CONFIG_DEFAULT_PREFIX + ConfigConstants.SECURITY_AUDIT_EXTERNAL_OPENSEARCH_PASSWORD,
183-
"admin"
184-
)
171+
.put(ConfigConstants.SECURITY_AUDIT_CONFIG_DEFAULT_PREFIX + ConfigConstants.SECURITY_AUDIT_CONFIG_USERNAME, "admin")
172+
.put(ConfigConstants.SECURITY_AUDIT_CONFIG_DEFAULT_PREFIX + ConfigConstants.SECURITY_AUDIT_CONFIG_PASSWORD, "admin")
185173
.build();
186174

187175
setup(additionalSettings);

src/test/java/org/opensearch/security/auditlog/sink/WebhookAuditLogTest.java

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -774,6 +774,162 @@ private SSLContext createSSLContext() throws Exception {
774774
return sslContext;
775775
}
776776

777+
@Test
778+
public void basicAuthPostTest() throws Exception {
779+
TestHttpHandler handler = new TestHttpHandler();
780+
781+
int port = findFreePort();
782+
server = ServerBootstrap.bootstrap()
783+
.setListenerPort(port)
784+
.setHttpProcessor(HttpProcessors.server("Test/1.1"))
785+
.setRequestRouter((request, context) -> handler)
786+
.create();
787+
788+
server.start();
789+
790+
String url = "http://localhost:" + port + "/endpoint";
791+
String username = "test_user";
792+
String password = "test_password";
793+
794+
// Test with basic auth credentials - POST JSON
795+
Settings settings = Settings.builder()
796+
.put("plugins.security.audit.config.webhook.url", url)
797+
.put("plugins.security.audit.config.webhook.format", "json")
798+
.put("plugins.security.audit.config.username", username)
799+
.put("plugins.security.audit.config.password", password)
800+
.put("path.home", ".")
801+
.put(
802+
SSLConfigConstants.SECURITY_SSL_TRANSPORT_TRUSTSTORE_FILEPATH,
803+
FileHelper.getAbsoluteFilePathFromClassPath("auditlog/truststore.jks")
804+
)
805+
.build();
806+
807+
LoggingSink fallback = new LoggingSink("test", Settings.EMPTY, null, null);
808+
WebhookSink auditlog = new WebhookSink("name", settings, ConfigConstants.SECURITY_AUDIT_CONFIG_DEFAULT, null, fallback);
809+
AuditMessage msg = MockAuditMessageFactory.validAuditMessage();
810+
auditlog.store(msg);
811+
812+
// Verify request was made
813+
assertThat(handler.method, is("POST"));
814+
Assert.assertNotNull(handler.body);
815+
Assert.assertTrue(handler.body.contains("{"));
816+
assertStringContainsAllKeysAndValues(handler.body);
817+
818+
// Verify Authorization header is present and correct
819+
Assert.assertNotNull(handler.headers);
820+
Assert.assertTrue(handler.headers.containsKey("Authorization"));
821+
String authHeader = handler.headers.get("Authorization");
822+
Assert.assertTrue(authHeader.startsWith("Basic "));
823+
824+
// Decode and verify credentials
825+
String encodedCredentials = authHeader.substring("Basic ".length());
826+
String decodedCredentials = new String(java.util.Base64.getDecoder().decode(encodedCredentials), StandardCharsets.UTF_8);
827+
Assert.assertEquals(username + ":" + password, decodedCredentials);
828+
829+
// no message stored on fallback
830+
assertThat(fallback.messages.size(), is(0));
831+
auditlog.close();
832+
server.awaitTermination(TimeValue.ofSeconds(3));
833+
}
834+
835+
@Test
836+
public void basicAuthGetTest() throws Exception {
837+
TestHttpHandler handler = new TestHttpHandler();
838+
839+
int port = findFreePort();
840+
server = ServerBootstrap.bootstrap()
841+
.setListenerPort(port)
842+
.setHttpProcessor(HttpProcessors.server("Test/1.1"))
843+
.setRequestRouter((request, context) -> handler)
844+
.create();
845+
846+
server.start();
847+
848+
String url = "http://localhost:" + port + "/endpoint";
849+
String username = "test_user";
850+
String password = "test_password";
851+
852+
// Test with basic auth credentials - GET
853+
Settings settings = Settings.builder()
854+
.put("plugins.security.audit.config.webhook.url", url)
855+
.put("plugins.security.audit.config.webhook.format", "URL_PARAMETER_GET")
856+
.put("plugins.security.audit.config.username", username)
857+
.put("plugins.security.audit.config.password", password)
858+
.put("path.home", ".")
859+
.put(
860+
SSLConfigConstants.SECURITY_SSL_TRANSPORT_TRUSTSTORE_FILEPATH,
861+
FileHelper.getAbsoluteFilePathFromClassPath("auditlog/truststore.jks")
862+
)
863+
.build();
864+
865+
LoggingSink fallback = new LoggingSink("test", Settings.EMPTY, null, null);
866+
WebhookSink auditlog = new WebhookSink("name", settings, ConfigConstants.SECURITY_AUDIT_CONFIG_DEFAULT, null, fallback);
867+
AuditMessage msg = MockAuditMessageFactory.validAuditMessage();
868+
auditlog.store(msg);
869+
870+
// Verify request was made with GET method
871+
assertThat(handler.method, is("GET"));
872+
873+
// Verify Authorization header is present and correct
874+
Assert.assertNotNull(handler.headers);
875+
Assert.assertTrue(handler.headers.containsKey("Authorization"));
876+
String authHeader = handler.headers.get("Authorization");
877+
Assert.assertTrue(authHeader.startsWith("Basic "));
878+
879+
// Decode and verify credentials
880+
String encodedCredentials = authHeader.substring("Basic ".length());
881+
String decodedCredentials = new String(java.util.Base64.getDecoder().decode(encodedCredentials), StandardCharsets.UTF_8);
882+
Assert.assertEquals(username + ":" + password, decodedCredentials);
883+
884+
auditlog.close();
885+
server.awaitTermination(TimeValue.ofSeconds(3));
886+
}
887+
888+
@Test
889+
public void webhookWithoutAuthTest() throws Exception {
890+
TestHttpHandler handler = new TestHttpHandler();
891+
892+
int port = findFreePort();
893+
server = ServerBootstrap.bootstrap()
894+
.setListenerPort(port)
895+
.setHttpProcessor(HttpProcessors.server("Test/1.1"))
896+
.setRequestRouter((request, context) -> handler)
897+
.create();
898+
899+
server.start();
900+
901+
String url = "http://localhost:" + port + "/endpoint";
902+
903+
// Test without credentials - should not have Authorization header
904+
Settings settings = Settings.builder()
905+
.put("plugins.security.audit.config.webhook.url", url)
906+
.put("plugins.security.audit.config.webhook.format", "json")
907+
.put("path.home", ".")
908+
.put(
909+
SSLConfigConstants.SECURITY_SSL_TRANSPORT_TRUSTSTORE_FILEPATH,
910+
FileHelper.getAbsoluteFilePathFromClassPath("auditlog/truststore.jks")
911+
)
912+
.build();
913+
914+
LoggingSink fallback = new LoggingSink("test", Settings.EMPTY, null, null);
915+
WebhookSink auditlog = new WebhookSink("name", settings, ConfigConstants.SECURITY_AUDIT_CONFIG_DEFAULT, null, fallback);
916+
AuditMessage msg = MockAuditMessageFactory.validAuditMessage();
917+
auditlog.store(msg);
918+
919+
// Verify request was made
920+
assertThat(handler.method, is("POST"));
921+
Assert.assertNotNull(handler.body);
922+
923+
// Verify Authorization header is NOT present
924+
Assert.assertFalse(handler.headers.containsKey("Authorization"));
925+
926+
// no message stored on fallback
927+
assertThat(fallback.messages.size(), is(0));
928+
929+
auditlog.close();
930+
server.awaitTermination(TimeValue.ofSeconds(3));
931+
}
932+
777933
private void assertStringContainsAllKeysAndValues(String in) {
778934
Assert.assertTrue(in, in.contains(AuditMessage.FORMAT_VERSION));
779935
Assert.assertTrue(in, in.contains(AuditMessage.CATEGORY));

0 commit comments

Comments
 (0)