Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Kerberos support #511

Merged
merged 10 commits into from
Aug 14, 2024
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
}
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()) {
sanzmauro marked this conversation as resolved.
Show resolved Hide resolved
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("");
chillaq marked this conversation as resolved.
Show resolved Hide resolved
_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
Loading