Skip to content

Commit

Permalink
Merge pull request #511 from splitio/kerberos-support
Browse files Browse the repository at this point in the history
Kerberos support
  • Loading branch information
chillaq authored Aug 14, 2024
2 parents 23074ab + 97bf31b commit e388362
Show file tree
Hide file tree
Showing 12 changed files with 499 additions and 7 deletions.
2 changes: 1 addition & 1 deletion client/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<parent>
<groupId>io.split.client</groupId>
<artifactId>java-client-parent</artifactId>
<version>4.12.1</version>
<version>4.13.0-rc1</version>
</parent>
<artifactId>java-client</artifactId>
<packaging>jar</packaging>
Expand Down
24 changes: 22 additions & 2 deletions client/src/main/java/io/split/client/SplitClientConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import io.split.integrations.IntegrationsConfig;
import io.split.storages.enums.OperationMode;
import io.split.storages.enums.StorageMode;
import io.split.service.HttpAuthScheme;
import org.apache.hc.core5.http.HttpHost;
import pluggable.CustomStorageWrapper;

Expand Down Expand Up @@ -91,6 +92,7 @@ public class SplitClientConfig {
private final HashSet<String> _flagSetsFilter;
private final int _invalidSets;
private final CustomHeaderDecorator _customHeaderDecorator;
private final HttpAuthScheme _authScheme;


public static Builder builder() {
Expand Down Expand Up @@ -148,7 +150,8 @@ private SplitClientConfig(String endpoint,
ThreadFactory threadFactory,
HashSet<String> flagSetsFilter,
int invalidSets,
CustomHeaderDecorator customHeaderDecorator) {
CustomHeaderDecorator customHeaderDecorator,
HttpAuthScheme authScheme) {
_endpoint = endpoint;
_eventsEndpoint = eventsEndpoint;
_featuresRefreshRate = pollForFeatureChangesEveryNSeconds;
Expand Down Expand Up @@ -201,6 +204,7 @@ private SplitClientConfig(String endpoint,
_flagSetsFilter = flagSetsFilter;
_invalidSets = invalidSets;
_customHeaderDecorator = customHeaderDecorator;
_authScheme = authScheme;

Properties props = new Properties();
try {
Expand Down Expand Up @@ -408,6 +412,9 @@ public int getInvalidSets() {
public CustomHeaderDecorator customHeaderDecorator() {
return _customHeaderDecorator;
}
public HttpAuthScheme authScheme() {
return _authScheme;
}

public static final class Builder {

Expand Down Expand Up @@ -466,6 +473,7 @@ public static final class Builder {
private HashSet<String> _flagSetsFilter = new HashSet<>();
private int _invalidSetsCount = 0;
private CustomHeaderDecorator _customHeaderDecorator = null;
private HttpAuthScheme _authScheme = null;

public Builder() {
}
Expand Down Expand Up @@ -960,6 +968,17 @@ public Builder customHeaderDecorator(CustomHeaderDecorator customHeaderDecorator
return this;
}

/**
* Authentication Scheme
*
* @param authScheme
* @return this builder
*/
public Builder authScheme(HttpAuthScheme authScheme) {
_authScheme = authScheme;
return this;
}

/**
* Thread Factory
*
Expand Down Expand Up @@ -1120,7 +1139,8 @@ public SplitClientConfig build() {
_threadFactory,
_flagSetsFilter,
_invalidSetsCount,
_customHeaderDecorator);
_customHeaderDecorator,
_authScheme);
}
}
}
9 changes: 9 additions & 0 deletions client/src/main/java/io/split/client/SplitFactoryImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,10 @@
import io.split.engine.segments.SegmentChangeFetcher;
import io.split.engine.segments.SegmentSynchronizationTaskImp;
import io.split.integrations.IntegrationsConfig;
import io.split.service.HttpAuthScheme;
import io.split.service.SplitHttpClient;
import io.split.service.SplitHttpClientImpl;
import io.split.service.SplitHttpClientKerberosImpl;
import io.split.storages.SegmentCache;
import io.split.storages.SegmentCacheConsumer;
import io.split.storages.SegmentCacheProducer;
Expand Down Expand Up @@ -525,6 +527,13 @@ private static SplitHttpClient buildSplitHttpClient(String apiToken, SplitClient
httpClientbuilder = setupProxy(httpClientbuilder, config);
}

if (config.authScheme() == HttpAuthScheme.KERBEROS) {
return SplitHttpClientKerberosImpl.create(
requestDecorator,
apiToken,
sdkMetadata);

}
return SplitHttpClientImpl.create(httpClientbuilder.build(),
requestDecorator,
apiToken,
Expand Down
5 changes: 5 additions & 0 deletions client/src/main/java/io/split/service/HttpAuthScheme.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package io.split.service;

public enum HttpAuthScheme {
KERBEROS
}
211 changes: 211 additions & 0 deletions client/src/main/java/io/split/service/SplitHttpClientKerberosImpl.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
package io.split.service;

import io.split.client.RequestDecorator;
import io.split.client.dtos.SplitHttpResponse;
import io.split.client.utils.SDKMetadata;
import io.split.engine.common.FetchOptions;

import org.apache.hc.client5.http.classic.methods.HttpGet;
import org.apache.hc.core5.http.Header;
import org.apache.hc.core5.http.HttpEntity;
import org.apache.hc.core5.http.HttpRequest;
import org.apache.hc.core5.http.io.entity.EntityUtils;
import org.apache.hc.core5.http.message.BasicHeader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.InputStreamReader;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URI;
import java.net.HttpURLConnection;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

public class SplitHttpClientKerberosImpl implements SplitHttpClient {

private static final Logger _log = LoggerFactory.getLogger(SplitHttpClientKerberosImpl.class);
private static final String HEADER_CACHE_CONTROL_NAME = "Cache-Control";
private static final String HEADER_CACHE_CONTROL_VALUE = "no-cache";
private static final String HEADER_API_KEY = "Authorization";
private static final String HEADER_CLIENT_KEY = "SplitSDKClientKey";
private static final String HEADER_CLIENT_MACHINE_NAME = "SplitSDKMachineName";
private static final String HEADER_CLIENT_MACHINE_IP = "SplitSDKMachineIP";
private static final String HEADER_CLIENT_VERSION = "SplitSDKVersion";

private final RequestDecorator _requestDecorator;
private final String _apikey;
private final SDKMetadata _metadata;

public static SplitHttpClientKerberosImpl create(RequestDecorator requestDecorator,
String apikey,
SDKMetadata metadata) {
return new SplitHttpClientKerberosImpl(requestDecorator, apikey, metadata);
}

SplitHttpClientKerberosImpl(RequestDecorator requestDecorator,
String apikey,
SDKMetadata metadata) {
_requestDecorator = requestDecorator;
_apikey = apikey;
_metadata = metadata;
}

public synchronized SplitHttpResponse get(URI uri, FetchOptions options, Map<String, List<String>> additionalHeaders) {
HttpURLConnection getHttpURLConnection = null;
try {
getHttpURLConnection = (HttpURLConnection) uri.toURL().openConnection();
return doGet(getHttpURLConnection, options, additionalHeaders);
} catch (Exception e) {
throw new IllegalStateException(String.format("Problem in http get operation: %s", e), e);
} finally {
try {
if (getHttpURLConnection != null) {
getHttpURLConnection.disconnect();
}
} catch (Exception e) {
_log.error(String.format("Could not close HTTP URL Connection: %s", e), e);
}
}
}
public SplitHttpResponse doGet(HttpURLConnection getHttpURLConnection, FetchOptions options, Map<String, List<String>> additionalHeaders) {
try {
getHttpURLConnection.setRequestMethod("GET");
setBasicHeaders(getHttpURLConnection);
setAdditionalAndDecoratedHeaders(getHttpURLConnection, additionalHeaders);

if (options.cacheControlHeadersEnabled()) {
getHttpURLConnection.setRequestProperty(HEADER_CACHE_CONTROL_NAME, HEADER_CACHE_CONTROL_VALUE);
}

_log.debug(String.format("Request Headers: %s", getHttpURLConnection.getRequestProperties()));

int responseCode = getHttpURLConnection.getResponseCode();

if (_log.isDebugEnabled()) {
_log.debug(String.format("[%s] %s. Status code: %s",
getHttpURLConnection.getRequestMethod(),
getHttpURLConnection.getURL().toString(),
responseCode));
}

String statusMessage = "";
if (responseCode < HttpURLConnection.HTTP_OK || responseCode >= HttpURLConnection.HTTP_MULT_CHOICE) {
_log.warn(String.format("Response status was: %s. Reason: %s", responseCode,
getHttpURLConnection.getResponseMessage()));
statusMessage = getHttpURLConnection.getResponseMessage();
}

InputStreamReader inputStreamReader = new InputStreamReader(getHttpURLConnection.getInputStream());
BufferedReader br = new BufferedReader(inputStreamReader);
String strCurrentLine;
StringBuilder bld = new StringBuilder();
while ((strCurrentLine = br.readLine()) != null) {
bld.append(strCurrentLine);
}
String responseBody = bld.toString();
inputStreamReader.close();
return new SplitHttpResponse(responseCode,
statusMessage,
responseBody,
getResponseHeaders(getHttpURLConnection));
} catch (Exception e) {
throw new IllegalStateException(String.format("Problem in http get operation: %s", e), e);
}
}

public synchronized SplitHttpResponse post(URI uri, HttpEntity entity, Map<String, List<String>> additionalHeaders) throws IOException {
HttpURLConnection postHttpURLConnection = null;
try {
postHttpURLConnection = (HttpURLConnection) uri.toURL().openConnection();
return doPost(postHttpURLConnection, entity, additionalHeaders);
} catch (Exception e) {
throw new IllegalStateException(String.format("Problem in http post operation: %s", e), e);
} finally {
try {
if (postHttpURLConnection != null) {
postHttpURLConnection.disconnect();
}
} catch (Exception e) {
_log.error(String.format("Could not close URL Connection: %s", e), e);
}
}
}

public SplitHttpResponse doPost(HttpURLConnection postHttpURLConnection,
HttpEntity entity,
Map<String, List<String>> additionalHeaders) {
try {
postHttpURLConnection.setRequestMethod("POST");
setBasicHeaders(postHttpURLConnection);
setAdditionalAndDecoratedHeaders(postHttpURLConnection, additionalHeaders);

postHttpURLConnection.setRequestProperty("Accept-Encoding", "gzip");
postHttpURLConnection.setRequestProperty("Content-Type", "application/json");
_log.debug(String.format("Request Headers: %s", postHttpURLConnection.getRequestProperties()));

postHttpURLConnection.setDoOutput(true);
String postBody = EntityUtils.toString(entity);
OutputStream os = postHttpURLConnection.getOutputStream();
os.write(postBody.getBytes(StandardCharsets.UTF_8));
os.flush();
os.close();
_log.debug(String.format("Posting: %s", postBody));

int responseCode = postHttpURLConnection.getResponseCode();
String statusMessage = "";
if (responseCode < HttpURLConnection.HTTP_OK || responseCode >= HttpURLConnection.HTTP_MULT_CHOICE) {
statusMessage = postHttpURLConnection.getResponseMessage();
_log.warn(String.format("Response status was: %s. Reason: %s", responseCode,
statusMessage));
}
return new SplitHttpResponse(responseCode, statusMessage, "", getResponseHeaders(postHttpURLConnection));
} catch (Exception e) {
throw new IllegalStateException(String.format("Problem in http post operation: %s", e), e);
}
}

private void setBasicHeaders(HttpURLConnection urlConnection) {
urlConnection.setRequestProperty(HEADER_API_KEY, "Bearer " + _apikey);
urlConnection.setRequestProperty(HEADER_CLIENT_VERSION, _metadata.getSdkVersion());
urlConnection.setRequestProperty(HEADER_CLIENT_MACHINE_IP, _metadata.getMachineIp());
urlConnection.setRequestProperty(HEADER_CLIENT_MACHINE_NAME, _metadata.getMachineName());
urlConnection.setRequestProperty(HEADER_CLIENT_KEY, _apikey.length() > 4
? _apikey.substring(_apikey.length() - 4)
: _apikey);
}

private void setAdditionalAndDecoratedHeaders(HttpURLConnection urlConnection, Map<String, List<String>> additionalHeaders) {
if (additionalHeaders != null) {
for (Map.Entry<String, List<String>> entry : additionalHeaders.entrySet()) {
for (String value : entry.getValue()) {
urlConnection.setRequestProperty(entry.getKey(), value);
}
}
}
HttpRequest request = new HttpGet("");
_requestDecorator.decorateHeaders(request);
for (Header header : request.getHeaders()) {
urlConnection.setRequestProperty(header.getName(), header.getValue());
}
}

private Header[] getResponseHeaders(HttpURLConnection urlConnection) {
List<BasicHeader> responseHeaders = new ArrayList<>();
Map<String, List<String>> map = urlConnection.getHeaderFields();
for (Map.Entry<String, List<String>> entry : map.entrySet()) {
if (entry.getKey() != null) {
BasicHeader responseHeader = new BasicHeader(entry.getKey(), entry.getValue());
responseHeaders.add(responseHeader);
}
}
return responseHeaders.toArray(new Header[0]);
}
@Override
public void close() throws IOException {
// Added for compatibility with HttpSplitClient, no action needed as URLConnection objects are closed.
}
}
13 changes: 13 additions & 0 deletions client/src/test/java/io/split/client/SplitClientConfigTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import io.split.client.impressions.ImpressionsManager;
import io.split.client.dtos.RequestContext;
import io.split.integrations.IntegrationsConfig;
import io.split.service.HttpAuthScheme;
import org.junit.Assert;
import org.junit.Test;
import org.mockito.Mockito;
Expand Down Expand Up @@ -254,4 +255,16 @@ public Map<String, List<String>> getHeaderOverrides(RequestContext context) {
Assert.assertNull(config2.customHeaderDecorator());

}

@Test
public void checkExpectedAuthScheme() {
SplitClientConfig cfg = SplitClientConfig.builder()
.authScheme(HttpAuthScheme.KERBEROS)
.build();
Assert.assertEquals(HttpAuthScheme.KERBEROS, cfg.authScheme());

cfg = SplitClientConfig.builder()
.build();
Assert.assertEquals(null, cfg.authScheme());
}
}
20 changes: 20 additions & 0 deletions client/src/test/java/io/split/client/SplitFactoryImplTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@

import io.split.client.impressions.ImpressionsManager;
import io.split.client.utils.FileTypeEnum;
import io.split.client.utils.SDKMetadata;
import io.split.integrations.IntegrationsConfig;
import io.split.service.HttpAuthScheme;
import io.split.service.SplitHttpClientKerberosImpl;
import io.split.storages.enums.OperationMode;
import io.split.storages.pluggable.domain.UserStorageWrapper;
import io.split.telemetry.storage.TelemetryStorage;
Expand All @@ -22,6 +25,8 @@
import java.lang.reflect.Modifier;
import java.net.URISyntaxException;

import static io.split.client.SplitClientConfig.splitSdkVersion;

public class SplitFactoryImplTest extends TestCase {
public static final String API_KEY ="29013ionasdasd09u";
public static final String ENDPOINT = "https://sdk.split-stage.io";
Expand Down Expand Up @@ -344,4 +349,19 @@ public void testLocalhosJsonInputStreamNullAndFileTypeNull() throws URISyntaxExc
Object splitChangeFetcher = method.invoke(splitFactory, splitClientConfig);
Assert.assertTrue(splitChangeFetcher instanceof LegacyLocalhostSplitChangeFetcher);
}

@Test
public void testFactoryKerberosInstance() throws URISyntaxException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
SplitClientConfig splitClientConfig = SplitClientConfig.builder()
.setBlockUntilReadyTimeout(10000)
.authScheme(HttpAuthScheme.KERBEROS)
.build();
SplitFactoryImpl splitFactory = new SplitFactoryImpl("asdf", splitClientConfig);

Method method = SplitFactoryImpl.class.getDeclaredMethod("buildSplitHttpClient", String.class,
SplitClientConfig.class, SDKMetadata.class, RequestDecorator.class);
method.setAccessible(true);
Object SplitHttpClient = method.invoke(splitFactory, "asdf", splitClientConfig, new SDKMetadata(splitSdkVersion, "", ""), new RequestDecorator(null));
Assert.assertTrue(SplitHttpClient instanceof SplitHttpClientKerberosImpl);
}
}
Loading

0 comments on commit e388362

Please sign in to comment.