Skip to content

Commit

Permalink
Adds Lookup tables, sources username from Basic authentication header (
Browse files Browse the repository at this point in the history
…#37)

* WIP commit

* Adds lookup table stuff

* Properly send unauthorized header when unauthorized and bad request otherwise

* Adds comment about removing AUTHORIZATION header

* Adds note about lookup tables
  • Loading branch information
StrongestNumber9 authored Apr 18, 2024
1 parent 50b2a2a commit 3a3cb4b
Show file tree
Hide file tree
Showing 23 changed files with 693 additions and 102 deletions.
21 changes: 16 additions & 5 deletions README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,22 @@ server.maxContentLength,262144,How big requests are allowed in bytes
relp.target,127.0.0.1,RELP server address
relp.port,601,RELP server port
relp.reconnectInterval,10000,How long to wait before reconnecting in milliseconds
relp.appName,lsh_01,Appname to use in RELP records
relp.hostname,localhost,Hostname to use in RELP records
healthcheck.enabled,true,Sets if an internal healthcheck endpoint is enabled.
healthcheck.url,/healthcheck,An internal healthcheck endpoint that will always reply 200 ok regardless of security settings. Accessing this url won't generate any events.
security.authRequired,true,Sets whether Basic HTTP Authorization headers are required.
security.authRequired,true,Sets whether Basic HTTP Authorization headers are required. Username for lookups will be empty string '' if set to false.
credentials.file,etc/credentials.json,A json file with array of identity:credential mappings

lookups.hostname.file,etc/hostname.json,Path to username-to-hostname lookup table
lookups.appname.file,etc/appname.json,Path to username-to-appname lookup table
|===

=== Lookup tables

By default, the appname and hostname lookup tables are expected to be in `etc/appname.json` and `etc/hostname.json` respectively.

Empty string '' will be used for looking up values when authentication is not required, otherwise it will use one extracted from Authorization header.

The index is the username and value is what it should map the values to. The value of 'nomatch' will be used if lookup fails.

== Limitations

Developed on and requires java 11
Expand All @@ -56,7 +63,11 @@ Expected files are:

- `/config/config.properties`

- `/config/credentials.json`
- `/config/credentials.json`

- `/config/hostname.json`

- `/config/appname.json`

- Optional: Custom `log4j2.xml` can be provided by placing it in `/config/log4j2.xml`, otherwise default settings will be used.

Expand Down
13 changes: 5 additions & 8 deletions docker-entrypoint.sh
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
#!/bin/bash
if [ ! -f /config/config.properties ]; then
echo "ERROR: Missing '/config/config.properties', refusing to continue";
for file in config.properties credentials.json hostname.json appname.json; do
if [ ! -f "/config/${file}" ]; then
echo "ERROR: Missing '/config/${file}', refusing to continue";
exit 1;
fi;

if [ ! -f /config/credentials.json ]; then
echo "ERROR: Missing '/config/credentials.json', refusing to continue";
exit 1;
fi;
fi;
done;

if [ -f /config/log4j2.xml ]; then
LOG4J_FILE="/config/log4j2.xml";
Expand Down
10 changes: 10 additions & 0 deletions etc/appname.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"nomatch": "fallback-appname",
"type": "array",
"table": [
{
"index": "ExampleUser",
"value": "ExampleUsers-ExampleApp"
}
]
}
7 changes: 4 additions & 3 deletions etc/config.properties
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@ healthcheck.url=/healthcheck
relp.target=127.0.0.1
relp.port=601
relp.reconnectInterval=10000
relp.appName=lsh_01
relp.hostname=localhost

security.authRequired=true

credentials.file=etc/credentials.json
credentials.file=etc/credentials.json

lookups.hostname.file=etc/hostname.json
lookups.appname.file=etc/appname.json
10 changes: 10 additions & 0 deletions etc/hostname.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"nomatch": "fallback-hostname.example.com",
"type": "array",
"table": [
{
"index": "ExampleUser",
"value": "example-user-host.example.com"
}
]
}
12 changes: 12 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,18 @@
</exclusion>
</exclusions>
</dependency>
<!-- lookups -->
<dependency>
<groupId>com.teragrep</groupId>
<artifactId>jlt_01</artifactId>
<version>1.0.1</version>
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- netty -->
<dependency>
<groupId>io.netty</groupId>
Expand Down
6 changes: 4 additions & 2 deletions src/main/java/com/teragrep/lsh_01/IMessageHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
*/
package com.teragrep.lsh_01;

import com.teragrep.lsh_01.authentication.Subject;

import java.util.Map;

/**
Expand All @@ -33,13 +35,13 @@ public interface IMessageHandler {
* @param headers
* @param body
*/
boolean onNewMessage(String remoteAddress, Map<String, String> headers, String body);
boolean onNewMessage(String remoteAddress, Subject subject, Map<String, String> headers, String body);

/**
* @param token
* @return
*/
boolean validatesToken(String token);
Subject asSubject(String token);

boolean requiresToken();

Expand Down
10 changes: 9 additions & 1 deletion src/main/java/com/teragrep/lsh_01/Main.java
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,13 @@ public static void main(String[] args) {
SecurityConfig securityConfig = new SecurityConfig();
BasicAuthentication basicAuthentication = new BasicAuthenticationFactory().create();
InternalEndpointUrlConfig internalEndpointUrlConfig = new InternalEndpointUrlConfig();
LookupConfig lookupConfig = new LookupConfig();
try {
nettyConfig.validate();
relpConfig.validate();
securityConfig.validate();
internalEndpointUrlConfig.validate();
lookupConfig.validate();
}
catch (IllegalArgumentException e) {
LOGGER.error("Can't parse config properly: {}", e.getMessage());
Expand All @@ -48,8 +50,14 @@ public static void main(String[] args) {
LOGGER.info("Got server config: <[{}]>", nettyConfig);
LOGGER.info("Got relp config: <[{}]>", relpConfig);
LOGGER.info("Got internal endpoint config: <[{}]>", internalEndpointUrlConfig);
LOGGER.info("Got lookup table config: <[{}]>", lookupConfig);
LOGGER.info("Authentication required: <[{}]>", securityConfig.authRequired);
RelpConversion relpConversion = new RelpConversion(relpConfig, securityConfig, basicAuthentication);
RelpConversion relpConversion = new RelpConversion(
relpConfig,
securityConfig,
basicAuthentication,
lookupConfig
);
try (
NettyHttpServer server = new NettyHttpServer(
nettyConfig,
Expand Down
54 changes: 35 additions & 19 deletions src/main/java/com/teragrep/lsh_01/MessageProcessor.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@
*/
package com.teragrep.lsh_01;

import com.teragrep.lsh_01.authentication.*;
import com.teragrep.lsh_01.config.InternalEndpointUrlConfig;
import com.teragrep.rlo_14.*;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
Expand Down Expand Up @@ -87,19 +87,40 @@ public void run() {
LOGGER.debug("Healthcheck endpoint called");
response = generateResponse(messageHandler.responseHeaders());
}
else if (isTokenOk()) {
LOGGER.debug("Processing message");
response = processMessage();
}
else if (!req.headers().contains(HttpHeaderNames.AUTHORIZATION)) {
LOGGER.debug("Required authorization not provided; requesting authentication.");
response = generateAuthenticationRequestResponse();
}
else {
LOGGER.debug("Invalid authorization; rejecting request.");
response = generateFailedResponse(HttpResponseStatus.UNAUTHORIZED);
if (messageHandler.requiresToken()) {
if (req.headers().contains(HttpHeaderNames.AUTHORIZATION)) {
HttpResponse response1;
try {
Subject subject = messageHandler
.asSubject(req.headers().get(HttpHeaderNames.AUTHORIZATION));
// Headers are inserted to structured data so remove it to prevent credentials leaking
req.headers().remove(HttpHeaderNames.AUTHORIZATION);
if (subject.isStub()) {
LOGGER.debug("Authentication failed; rejecting request.");
response1 = generateFailedResponse(HttpResponseStatus.UNAUTHORIZED);
}
else {
LOGGER.debug("Processing message");
response1 = processMessage(subject);
}
}
catch (Exception e) {
LOGGER.debug("Invalid authorization; rejecting request.");
response1 = generateFailedResponse(HttpResponseStatus.BAD_REQUEST);
}
response = response1;
}
else {
LOGGER.debug("Required authorization not provided; requesting authentication.");
response = generateAuthenticationRequestResponse();
}
}
else {
Subject subject = new SubjectAnonymous();
response = processMessage(subject);
}
}

ctx.writeAndFlush(response);
}
finally {
Expand All @@ -112,15 +133,10 @@ private boolean isInternalEndpoint() {
&& internalEndpointUrlConfig.healthcheckUrl.equals(req.uri());
}

private boolean isTokenOk() {
return !messageHandler.requiresToken()
|| messageHandler.validatesToken(req.headers().get(HttpHeaderNames.AUTHORIZATION));
}

private FullHttpResponse processMessage() {
private FullHttpResponse processMessage(Subject subject) {
final Map<String, String> formattedHeaders = formatHeaders(req.headers());
final String body = req.content().toString(UTF8_CHARSET);
if (messageHandler.onNewMessage(remoteAddress, formattedHeaders, body)) {
if (messageHandler.onNewMessage(remoteAddress, subject, formattedHeaders, body)) {
return generateResponse(messageHandler.responseHeaders());
}
else {
Expand Down
31 changes: 22 additions & 9 deletions src/main/java/com/teragrep/lsh_01/RelpConversion.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,13 @@
*/
package com.teragrep.lsh_01;

import com.teragrep.jlt_01.StringLookupTable;
import com.teragrep.lsh_01.authentication.BasicAuthentication;
import com.teragrep.lsh_01.authentication.Subject;
import com.teragrep.lsh_01.config.LookupConfig;
import com.teragrep.lsh_01.config.RelpConfig;
import com.teragrep.lsh_01.config.SecurityConfig;
import com.teragrep.lsh_01.lookup.LookupTableFactory;
import com.teragrep.rlo_14.*;
import com.teragrep.rlp_01.RelpBatch;
import com.teragrep.rlp_01.RelpConnection;
Expand All @@ -43,21 +47,30 @@ public class RelpConversion implements IMessageHandler {
private final RelpConfig relpConfig;
private final SecurityConfig securityConfig;
private final BasicAuthentication basicAuthentication;
private final LookupConfig lookupConfig;
private final StringLookupTable hostnameLookup;
private final StringLookupTable appnameLookup;

public RelpConversion(
RelpConfig relpConfig,
SecurityConfig securityConfig,
BasicAuthentication basicAuthentication
BasicAuthentication basicAuthentication,
LookupConfig lookupConfig
) {
this.relpConfig = relpConfig;
this.securityConfig = securityConfig;
this.relpConnection = new RelpConnection();
this.basicAuthentication = basicAuthentication;
this.lookupConfig = lookupConfig;
this.hostnameLookup = new LookupTableFactory().create(lookupConfig.hostnamePath);
this.appnameLookup = new LookupTableFactory().create(lookupConfig.appNamePath);
}

public boolean onNewMessage(String remoteAddress, Map<String, String> headers, String body) {
public boolean onNewMessage(String remoteAddress, Subject subject, Map<String, String> headers, String body) {
try {
sendMessage(body, headers);
sendMessage(
body, headers, hostnameLookup.lookup(subject.subject()), appnameLookup.lookup(subject.subject())
);
}
catch (Exception e) {
LOGGER.error("Unexpected error when sending a message: <{}>", e.getMessage(), e);
Expand All @@ -66,8 +79,8 @@ public boolean onNewMessage(String remoteAddress, Map<String, String> headers, S
return true;
}

public boolean validatesToken(String token) {
return basicAuthentication.isCredentialOk(token);
public Subject asSubject(String token) {
return basicAuthentication.asSubject(token);
}

public boolean requiresToken() {
Expand All @@ -76,7 +89,7 @@ public boolean requiresToken() {

public RelpConversion copy() {
LOGGER.debug("RelpConversion.copy called");
return new RelpConversion(relpConfig, securityConfig, basicAuthentication);
return new RelpConversion(relpConfig, securityConfig, basicAuthentication, lookupConfig);
}

public Map<String, String> responseHeaders() {
Expand Down Expand Up @@ -131,7 +144,7 @@ private void disconnect() {
isConnected = false;
}

private void sendMessage(String message, Map<String, String> headers) {
private void sendMessage(String message, Map<String, String> headers, String hostname, String appName) {
if (!isConnected) {
connect();
}
Expand All @@ -143,8 +156,8 @@ private void sendMessage(String message, Map<String, String> headers) {
}
SyslogMessage syslogMessage = new SyslogMessage()
.withTimestamp(time.toEpochMilli())
.withAppName(relpConfig.relpAppName)
.withHostname(relpConfig.relpHostname)
.withAppName(appName)
.withHostname(hostname)
.withFacility(Facility.USER)
.withSeverity(Severity.INFORMATIONAL)
.withMsg(message)
Expand Down
Loading

0 comments on commit 3a3cb4b

Please sign in to comment.