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 6 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
31 changes: 29 additions & 2 deletions client/src/main/java/io/split/client/SplitClientConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import java.util.List;
import java.util.Properties;
import java.util.concurrent.ThreadFactory;
import java.util.Locale;
import java.io.InputStream;

import static io.split.inputValidation.FlagSetsValidator.cleanup;
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 String _authScheme;
chillaq marked this conversation as resolved.
Show resolved Hide resolved


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,
String 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 String 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 String _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(String authScheme) {
chillaq marked this conversation as resolved.
Show resolved Hide resolved
_authScheme = authScheme;
return this;
}

/**
* Thread Factory
*
Expand Down Expand Up @@ -1068,6 +1087,13 @@ public SplitClientConfig build() {
_storageMode = StorageMode.PLUGGABLE;
}

if(_authScheme != null) {
if (!_authScheme.toLowerCase(Locale.ROOT).equals("kerberos")) {
throw new IllegalArgumentException("authScheme must be either null or `kerberos`.");
}
_authScheme = "kerberos";
}
chillaq marked this conversation as resolved.
Show resolved Hide resolved

return new SplitClientConfig(
_endpoint,
_eventsEndpoint,
Expand Down Expand Up @@ -1120,7 +1146,8 @@ public SplitClientConfig build() {
_threadFactory,
_flagSetsFilter,
_invalidSetsCount,
_customHeaderDecorator);
_customHeaderDecorator,
_authScheme);
}
}
}
8 changes: 8 additions & 0 deletions client/src/main/java/io/split/client/SplitFactoryImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
import io.split.integrations.IntegrationsConfig;
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 +526,13 @@ private static SplitHttpClient buildSplitHttpClient(String apiToken, SplitClient
httpClientbuilder = setupProxy(httpClientbuilder, config);
}

if (config.authScheme() != null) {
chillaq marked this conversation as resolved.
Show resolved Hide resolved
return SplitHttpClientKerberosImpl.create(
requestDecorator,
apiToken,
sdkMetadata);

}
return SplitHttpClientImpl.create(httpClientbuilder.build(),
requestDecorator,
apiToken,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
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.*;
import java.net.*;
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(SplitHttpClient.class);
chillaq marked this conversation as resolved.
Show resolved Hide resolved
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) throws URISyntaxException {
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 _get(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 _get(HttpURLConnection getHttpURLConnection, FetchOptions options, Map<String, List<String>> additionalHeaders) {
chillaq marked this conversation as resolved.
Show resolved Hide resolved
InputStreamReader inputStreamReader = null;
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 = new InputStreamReader(getHttpURLConnection.getInputStream());
BufferedReader br = new BufferedReader(inputStreamReader);
String strCurrentLine;
String responseBody = new String();
while ((strCurrentLine = br.readLine()) != null) {
responseBody = responseBody + strCurrentLine;
}
return new SplitHttpResponse(responseCode,
statusMessage,
responseBody,
getResponseHeaders(getHttpURLConnection));
} catch (Exception e) {
throw new IllegalStateException(String.format("Problem in http get operation: %s", e), e);
} finally {
try {
if (inputStreamReader != null) {
inputStreamReader.close();
}
} catch (Exception e) {
_log.error(String.format("Could not close HTTP Stream: %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 _post(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 _post(HttpURLConnection postHttpURLConnection,
chillaq marked this conversation as resolved.
Show resolved Hide resolved
HttpEntity entity,
Map<String, List<String>> additionalHeaders)
throws IOException {
try {
postHttpURLConnection.setRequestMethod("POST");
setBasicHeaders(postHttpURLConnection);
setAdditionalAndDecoratedHeaders(postHttpURLConnection, additionalHeaders);

if (postHttpURLConnection.getHeaderField("Accept-Encoding") == null) {
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());
}
request = null;
chillaq marked this conversation as resolved.
Show resolved Hide resolved
}

private Header[] getResponseHeaders(HttpURLConnection urlConnection) {
List<BasicHeader> responseHeaders = new ArrayList<BasicHeader>();
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 {

chillaq marked this conversation as resolved.
Show resolved Hide resolved
}
}
25 changes: 25 additions & 0 deletions client/src/test/java/io/split/client/SplitClientConfigTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -254,4 +254,29 @@ public Map<String, List<String>> getHeaderOverrides(RequestContext context) {
Assert.assertNull(config2.customHeaderDecorator());

}

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

cfg = SplitClientConfig.builder()
.authScheme("KERberos")
.build();
Assert.assertEquals("kerberos", cfg.authScheme());

cfg = SplitClientConfig.builder()
.build();
Assert.assertEquals(null, cfg.authScheme());
}

@Test(expected = IllegalArgumentException.class)
public void checkUnexpectedAuthScheme() {
SplitClientConfig cfg = SplitClientConfig.builder()
.authScheme("proxy")
.build();
Assert.assertEquals(null, cfg.authScheme());
}
}
Loading
Loading