Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
130 changes: 122 additions & 8 deletions common/src/main/java/com/skyflow/BaseVaultClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import com.skyflow.generated.common.rest.core.ObjectMappers;
import com.skyflow.logs.ErrorLogs;
import com.skyflow.logs.InfoLogs;
import com.skyflow.logs.WarningLogs;
import com.skyflow.serviceaccount.util.Token;
import com.skyflow.utils.BaseConstants;
import com.skyflow.utils.BaseUtils;
Expand All @@ -17,9 +18,12 @@
import io.github.cdimascio.dotenv.Dotenv;
import io.github.cdimascio.dotenv.DotenvException;
import okhttp3.ConnectionPool;
import okhttp3.Interceptor;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;

import java.net.HttpURLConnection;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
Expand All @@ -33,6 +37,17 @@ public class BaseVaultClient<V extends BaseVaultConfig> {
protected BaseCredentials finalCredentials;
protected String token;
protected String apiKey;
// The token whose regeneration after a 401 last failed. Guarded by this. Further 401s on that
// same token return without generating again, so an auth outage costs one generation per
// token instead of one per rejected request. Cleared whenever a token is stored.
private String refreshFailedFor;

/**
* A 401 is treated as token expiry only if the token the SDK sent has an {@code exp} at or
* before now plus this window. By the time the 401 arrives the gateway has already judged the
* token expired, so the window only has to absorb clock skew between this host and the gateway.
*/
protected static final long BEARER_TOKEN_EXPIRY_WINDOW_SECONDS = 60;

protected BaseVaultClient(V vaultConfig, BaseCredentials credentials) {
this.vaultConfig = vaultConfig;
Expand All @@ -43,18 +58,116 @@ protected V getVaultConfig() {
return vaultConfig;
}

/**
* The bearer token to send. Synchronized on the same monitor as {@link #setBearerToken(BaseCredentials)}
* and {@link #refreshBearerTokenAfterUnauthorized(String)}, so a request attempt on an OkHttp or
* batch thread sees the token those methods wrote, and waits for a regeneration in progress
* instead of sending the token it is replacing.
*/
protected synchronized String getToken() {
return token;
}

protected OkHttpClient buildSharedHttpClient(Supplier<String> tokenSupplier) {
return new OkHttpClient.Builder()
.connectionPool(new ConnectionPool(10, 1, TimeUnit.MINUTES))
.addInterceptor(chain -> {
Request requestWithAuth = chain.request().newBuilder()
.header("Authorization", "Bearer " + tokenSupplier.get())
.build();
return chain.proceed(requestWithAuth);
})
.addInterceptor(authInterceptor(tokenSupplier))
.build();
}

/**
* The auth interceptor shared by every OkHttp client in the SDK. It attaches the current bearer
* token to each attempt, reading it fresh from {@code tokenSupplier} so a token refreshed between
* attempts is picked up. When the vault answers {@code 401} and
* {@link #refreshBearerTokenAfterUnauthorized(String)} can supply a replacement, the rejected
* response is closed and the same request is sent once more with the new token. Any other
* status, and a {@code 401} the refresh declines, is returned as is.
*/
protected Interceptor authInterceptor(Supplier<String> tokenSupplier) {
return chain -> {
String sent = tokenSupplier.get();
Response response = chain.proceed(withBearer(chain.request(), sent));
if (response.code() != HttpURLConnection.HTTP_UNAUTHORIZED) {
return response;
}
String fresh = refreshBearerTokenAfterUnauthorized(sent);
if (fresh == null) {
return response;
}
response.close();
return chain.proceed(withBearer(chain.request(), fresh));
};
}

private static Request withBearer(Request request, String bearer) {
return request.newBuilder().header("Authorization", "Bearer " + bearer).build();
}

/**
* Called when the vault rejected {@code rejected} with {@code 401}. Returns the token to resend
* the request with, or {@code null} when the request must fail as it stands:
* <ul>
* <li>the effective credentials are a static token or an API key, which the SDK cannot replace;</li>
* <li>the rejected token is not at or past expiry (within {@link #BEARER_TOKEN_EXPIRY_WINDOW_SECONDS}),
* so the {@code 401} was not caused by expiry and a new token would not help;</li>
* <li>generating a replacement failed, or produced the very token that was rejected;</li>
* <li>a replacement for this same token was already attempted and failed, so the outcome is
* shared with every request still holding it. A later method call recovers through
* {@link #setBearerToken(BaseCredentials)}, which clears that record when it stores a token.</li>
* </ul>
* Synchronized with {@link #setBearerToken(BaseCredentials)} and {@link #getToken()} so concurrent
* {@code 401}s on one token share a single regeneration: a caller that finds the cached token
* already different from the one it sent reuses it without generating again.
*/
protected synchronized String refreshBearerTokenAfterUnauthorized(String rejected) {
BaseCredentials credentials = this.finalCredentials;
if (credentials == null || credentials.getApiKey() != null || credentials.getToken() != null) {
return null;
}
if (!Token.isExpiringWithin(rejected, BEARER_TOKEN_EXPIRY_WINDOW_SECONDS)) {
LogUtil.printInfoLog(InfoLogs.BEARER_TOKEN_REJECTED_NOT_DUE.getLog());
return null;
}
String cached = this.token;
if (cached != null && !cached.equals(rejected)) {
LogUtil.printInfoLog(InfoLogs.BEARER_TOKEN_REJECTED_REUSING_REGENERATED.getLog());
return cached;
}
// rejected is non-null here: a null token fails the expiry gate above.
if (rejected.equals(refreshFailedFor)) {
LogUtil.printInfoLog(InfoLogs.BEARER_TOKEN_REJECTED_REGENERATION_ALREADY_FAILED.getLog());
return null;
}
LogUtil.printInfoLog(InfoLogs.BEARER_TOKEN_REJECTED_REGENERATING.getLog());
try {
String fresh = generateBearerToken(credentials);
if (fresh == null || fresh.equals(rejected)) {
LogUtil.printWarningLog(WarningLogs.BEARER_TOKEN_REGENERATION_RETURNED_REJECTED_TOKEN.getLog());
refreshFailedFor = rejected;
return null;
}
storeToken(fresh);
return fresh;
} catch (SkyflowException e) {
LogUtil.printWarningLog(WarningLogs.BEARER_TOKEN_REGENERATION_AFTER_401_FAILED.getLog()
+ " httpCode=" + e.getHttpCode()
+ (e.getRequestId() != null ? " requestId=" + e.getRequestId() : ""));
refreshFailedFor = rejected;
return null;
}
}

/** Stores a token the SDK obtained and forgets any failed regeneration, so refresh can run again. */
private void storeToken(String fresh) {
this.token = fresh;
this.refreshFailedFor = null;
}

/** Single point that turns credentials into a bearer token; tests substitute it. */
protected String generateBearerToken(BaseCredentials credentials) throws SkyflowException {
return BaseUtils.generateBearerToken(credentials);
}

protected synchronized void prioritiseCredentials(BaseCredentials vaultSpecificCredentials) throws SkyflowException {
try {
BaseCredentials original = this.finalCredentials;
Expand All @@ -78,6 +191,7 @@ protected synchronized void prioritiseCredentials(BaseCredentials vaultSpecificC
if (original != null && !original.equals(this.finalCredentials)) {
token = null;
apiKey = null;
refreshFailedFor = null;
}
} catch (DotenvException e) {
throw new SkyflowException(ErrorCode.INVALID_INPUT.getCode(), BaseErrorMessage.EmptyCredentials.getMessage());
Expand All @@ -95,10 +209,10 @@ protected synchronized void setBearerToken(BaseCredentials vaultSpecificCredenti
LogUtil.printInfoLog(InfoLogs.USE_API_KEY.getLog());
token = this.finalCredentials.getApiKey();
} else if (token == null || token.trim().isEmpty()) {
token = BaseUtils.generateBearerToken(this.finalCredentials);
storeToken(generateBearerToken(this.finalCredentials));
} else if (Token.isExpired(token)) {
LogUtil.printInfoLog(InfoLogs.BEARER_TOKEN_EXPIRED.getLog());
token = BaseUtils.generateBearerToken(this.finalCredentials);
storeToken(generateBearerToken(this.finalCredentials));
} else {
LogUtil.printInfoLog(InfoLogs.REUSE_BEARER_TOKEN.getLog());
}
Expand Down
10 changes: 0 additions & 10 deletions common/src/main/java/com/skyflow/detect/DataSourceType.java

This file was deleted.

49 changes: 30 additions & 19 deletions common/src/main/java/com/skyflow/detect/DeidentifyFileRequest.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
import java.io.File;

/**
* Request for {@code deidentifyFile}. Supply the file through exactly one of {@code value} (with a
* {@code dataSource}), {@code file} or {@code filePath}. The two file forms are builder sugar that set
* {@code dataSource = BASE64}, encode the content into {@code value} when the request is sent, and infer
* {@code dataFormat} from the extension unless it is set explicitly.
* Request for {@code deidentifyFile}. Supply the file through exactly one of {@code filePath}, {@code file}
* or {@code value} (with a {@code dataSource}); more than one, or none, is rejected before any call. A local
* file given as {@code file} or {@code filePath} is read when the request is sent, sent as {@code BASE64}, and
* its {@code dataFormat} is inferred from the extension unless set explicitly.
*
* <p>Without {@code pollOptions} the call submits the run and returns its id. With {@code pollOptions} the
* SDK polls the run and, on success for a file-based request, writes the outputs to disk.
Expand All @@ -16,9 +16,10 @@
* the vault's defaults.
*/
public final class DeidentifyFileRequest {
private final DataSourceType dataSource;
private final DeidentifyFileSource dataSource;
private final String value;
private final File file;
private final String filePath;
private final FileDataFormat dataFormat;
private final String configurationId;
private final DetectConfiguration configuration;
Expand All @@ -29,6 +30,7 @@ private DeidentifyFileRequest(DeidentifyFileRequestBuilder builder) {
this.dataSource = builder.dataSource;
this.value = builder.value;
this.file = builder.file;
this.filePath = builder.filePath;
this.dataFormat = builder.dataFormat;
this.configurationId = builder.configurationId;
this.configuration = builder.configuration;
Expand All @@ -44,26 +46,34 @@ public static DeidentifyFileRequestBuilder builder() {
}

/**
* @return How {@code value} supplies the file. Required unless {@code file} or {@code filePath} was used.
* @return How {@code value} supplies the file: this SDK's {@code DeidentifySourceType} (skyvault: BASE64, SKYFLOW_ID,
* PRESIGNED_URL; flowvault: BASE64, PRESIGNED_URL). Required with {@code value}.
*/
public DataSourceType getDataSource() {
public DeidentifyFileSource getDataSource() {
return dataSource;
}

/**
* @return Base64 content, skyflow id or presigned URL, according to {@code dataSource}.
* @return The file as {@code dataSource} describes it: base64 content, a presigned URL or, on skyvault, a skyflow id.
*/
public String getValue() {
return value;
}

/**
* @return Local file to de-identify, when {@code file} or {@code filePath} was used.
* @return Local file to de-identify, when {@code file} was used.
*/
public File getFile() {
return file;
}

/**
* @return Path of the local file to de-identify, when {@code filePath} was used.
*/
public String getFilePath() {
return filePath;
}

/**
* @return Format of the file. Optional; inferred from the file extension or by the server when omitted.
*/
Expand Down Expand Up @@ -105,6 +115,7 @@ public String toString() {
"dataSource=" + dataSource +
", value=" + (value == null ? null : "<" + value.length() + " chars>") +
", file=" + file +
", filePath=" + filePath +
", dataFormat=" + dataFormat +
", configurationId=" + configurationId +
", configuration=" + configuration +
Expand All @@ -114,9 +125,10 @@ public String toString() {
}

public static final class DeidentifyFileRequestBuilder {
private DataSourceType dataSource;
private DeidentifyFileSource dataSource;
private String value;
private File file;
private String filePath;
private FileDataFormat dataFormat;
private String configurationId;
private DetectConfiguration configuration;
Expand All @@ -127,16 +139,17 @@ private DeidentifyFileRequestBuilder() {
}

/**
* @param dataSource How {@code value} supplies the file.
* @param dataSource How {@code value} supplies the file: this SDK's {@code DeidentifySourceType}
* (skyvault: BASE64, SKYFLOW_ID, PRESIGNED_URL; flowvault: BASE64, PRESIGNED_URL).
* @return this builder
*/
public DeidentifyFileRequestBuilder dataSource(DataSourceType dataSource) {
public DeidentifyFileRequestBuilder dataSource(DeidentifyFileSource dataSource) {
this.dataSource = dataSource;
return this;
}

/**
* @param value Base64 content, skyflow id or presigned URL, according to {@code dataSource}.
* @param value The file as {@code dataSource} describes it: base64 content, a presigned URL or, on skyvault, a skyflow id.
* @return this builder
*/
public DeidentifyFileRequestBuilder value(String value) {
Expand All @@ -145,23 +158,21 @@ public DeidentifyFileRequestBuilder value(String value) {
}

/**
* @param file Local file to de-identify. Sets {@code dataSource} to BASE64; the content is encoded when sent.
* @param file Local file to de-identify. Sent as BASE64; the content is encoded when sent.
* @return this builder
*/
public DeidentifyFileRequestBuilder file(File file) {
this.file = file;
if (file != null) {
this.dataSource = DataSourceType.BASE64;
}
return this;
}

/**
* @param filePath Path of a local file to de-identify. Same as {@link #file(File)}.
* @param filePath Path of a local file to de-identify. Sent as BASE64; the content is encoded when sent.
* @return this builder
*/
public DeidentifyFileRequestBuilder filePath(String filePath) {
return file(filePath == null ? null : new File(filePath));
this.filePath = filePath;
return this;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@
public final class DeidentifyFileResponse {
private final String runId;
private final DetectRunStatus status;
private final DataSourceType outputType;
private final DeidentifyFileSource outputType;
private final List<FileOutput> output;
private final Metrics metrics;
private final String message;

public DeidentifyFileResponse(String runId, DetectRunStatus status, DataSourceType outputType,
public DeidentifyFileResponse(String runId, DetectRunStatus status, DeidentifyFileSource outputType,
List<FileOutput> output, Metrics metrics, String message) {
this.runId = runId;
this.status = status;
Expand All @@ -42,9 +42,9 @@ public DetectRunStatus getStatus() {
}

/**
* @return How output files are delivered. Null unless polled to SUCCESS.
* @return How output files are delivered. Null unless polled to SUCCESS. A constant of this SDK's {@code DeidentifySourceType}: {@code BASE64} or {@code PRESIGNED_URL}.
*/
public DataSourceType getOutputType() {
public DeidentifyFileSource getOutputType() {
return outputType;
}

Expand Down
15 changes: 15 additions & 0 deletions common/src/main/java/com/skyflow/detect/DeidentifyFileSource.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.skyflow.detect;

/**
* How the file of {@code deidentifyFile} is supplied, and how a run reports its outputs. Each SDK ships its own
* {@code com.skyflow.detect.DeidentifySourceType} enum implementing this interface with the sources its vault
* type accepts: skyvault {@code BASE64}, {@code SKYFLOW_ID}, {@code PRESIGNED_URL}; flowvault {@code BASE64},
* {@code PRESIGNED_URL}. {@code getOutputType()} on {@code DeidentifyFileResponse} and {@code GetRunResponse}
* returns a constant of that same enum. The constant name is the wire value.
*/
public interface DeidentifyFileSource {
/**
* @return the wire value, which is the constant name
*/
String name();
}
2 changes: 1 addition & 1 deletion common/src/main/java/com/skyflow/detect/EntityType.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package com.skyflow.detect;

/**
* Entity types Skyflow Detect can identify. Also used to target entities in {@link RedactionLevel}.
* Entity types Skyflow Detect can identify. Also used to target entities in {@code RedactionLevel}.
*/
public enum EntityType {
NONE,
Expand Down
Loading
Loading