Skip to content

Commit

Permalink
Add SecretManager in anticipation of more secret managers
Browse files Browse the repository at this point in the history
Add SecretManager in anticipation of more secret managers
  • Loading branch information
chris9692 authored Jan 26, 2022
2 parents e31e314 + c10dd52 commit 7925f1a
Show file tree
Hide file tree
Showing 15 changed files with 152 additions and 39 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
import com.google.gson.JsonObject;
import com.linkedin.cdi.util.EncryptionUtils;
import com.linkedin.cdi.util.SecretManager;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.List;
Expand Down Expand Up @@ -85,10 +85,10 @@ public Map<String, String> getAsMap(State state) {

String token = "";
if (authentication.has(KEY_WORD_TOKEN)) {
token = EncryptionUtils.decryptGobblin(authentication.get(KEY_WORD_TOKEN).getAsString(), state);
token = SecretManager.getInstance(state).decrypt(authentication.get(KEY_WORD_TOKEN).getAsString());
} else {
String u = EncryptionUtils.decryptGobblin(SOURCE_CONN_USERNAME.get(state), state);
String p = EncryptionUtils.decryptGobblin(SOURCE_CONN_PASSWORD.get(state), state);
String u = SecretManager.getInstance(state).decrypt(SOURCE_CONN_USERNAME.get(state));
String p = SecretManager.getInstance(state).decrypt(SOURCE_CONN_PASSWORD.get(state));
token = u + ":" + p;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@ public JsonObject getDefaultValue() {

JsonObjectProperties MSTAGE_SCHEMA_CLEANSING = new JsonObjectProperties("ms.schema.cleansing");
SecondaryInputProperties MSTAGE_SECONDARY_INPUT = new SecondaryInputProperties("ms.secondary.input");
StringProperties MSTAGE_SECRET_MANAGER_CLASS = new StringProperties("ms.secret.manager.class", "com.linkedin.cdi.util.GobblinSecretManager");
JsonObjectProperties MSTAGE_SESSION_KEY_FIELD = new JsonObjectProperties("ms.session.key.field");

// default: 60 seconds, minimum: 0, maximum: -
Expand Down Expand Up @@ -355,6 +356,7 @@ protected String getValidNonblankWithDefault(State state) {
MSTAGE_S3_LIST_MAX_KEYS,
MSTAGE_SCHEMA_CLEANSING,
MSTAGE_SECONDARY_INPUT,
MSTAGE_SECRET_MANAGER_CLASS,
MSTAGE_SESSION_KEY_FIELD,
MSTAGE_SFTP_CONN_TIMEOUT_MILLIS,
MSTAGE_SOURCE_DATA_CHARACTER_SET,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import com.linkedin.cdi.keys.ExtractorKeys;
import com.linkedin.cdi.keys.JobKeys;
import com.linkedin.cdi.keys.S3Keys;
import com.linkedin.cdi.util.EncryptionUtils;
import com.linkedin.cdi.util.SecretManager;
import com.linkedin.cdi.util.WorkUnitStatus;
import java.net.URI;
import java.time.Duration;
Expand Down Expand Up @@ -198,8 +198,8 @@ public AwsCredentialsProvider getCredentialsProvider(State state) {
AwsCredentialsProvider credentialsProvider = AnonymousCredentialsProvider.create();
if (StringUtils.isNotBlank(s3SourceV2Keys.getAccessKey()) || StringUtils.isNotEmpty(s3SourceV2Keys.getSecretId())) {
AwsCredentials credentials =
AwsBasicCredentials.create(EncryptionUtils.decryptGobblin(s3SourceV2Keys.getAccessKey(), state),
EncryptionUtils.decryptGobblin(s3SourceV2Keys.getSecretId(), state));
AwsBasicCredentials.create(SecretManager.getInstance(state).decrypt(s3SourceV2Keys.getAccessKey()),
SecretManager.getInstance(state).decrypt(s3SourceV2Keys.getSecretId()));
credentialsProvider = StaticCredentialsProvider.create(credentials);
}
return credentialsProvider;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,17 @@
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonNull;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.google.gson.JsonPrimitive;
import com.linkedin.cdi.configuration.StaticConstants;
import com.linkedin.cdi.filter.JsonSchemaBasedFilter;
import com.linkedin.cdi.keys.ExtractorKeys;
import com.linkedin.cdi.keys.JobKeys;
import com.linkedin.cdi.keys.JsonExtractorKeys;
import com.linkedin.cdi.util.EncryptionUtils;
import com.linkedin.cdi.util.JsonUtils;
import com.linkedin.cdi.util.ParameterTypes;
import com.linkedin.cdi.util.SchemaBuilder;
import com.linkedin.cdi.util.SecretManager;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.Charset;
Expand All @@ -40,7 +38,6 @@
import org.apache.gobblin.configuration.WorkUnitState;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.testng.Assert;

import static com.linkedin.cdi.configuration.PropertyCollection.*;
import static com.linkedin.cdi.configuration.StaticConstants.*;
Expand Down Expand Up @@ -555,7 +552,7 @@ private JsonObject encryptJsonFields(String parentKey, JsonElement input) {
// this function assumes that the final value to be encrypted will always be a JsonPrimitive object and in case of
// of JsonObject it will iterate recursively.
if (value.isJsonPrimitive() && encryptionFields.contains(new JsonPrimitive(absoluteKey))) {
String valStr = EncryptionUtils.encryptGobblin(value.isJsonNull() ? "" : value.getAsString(), state);
String valStr = SecretManager.getInstance(state).encrypt(value.isJsonNull() ? "" : value.getAsString());
output.add(key, new JsonPrimitive(valStr));
} else if (value.isJsonObject()) {
output.add(key, encryptJsonFields(absoluteKey, value));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,14 @@
import com.linkedin.cdi.keys.JobKeys;
import com.linkedin.cdi.preprocessor.StreamProcessor;
import com.linkedin.cdi.util.DateTimeUtils;
import com.linkedin.cdi.util.EncryptionUtils;
import com.linkedin.cdi.util.HdfsReader;
import com.linkedin.cdi.util.InputStreamUtils;
import com.linkedin.cdi.util.JsonIntermediateSchema;
import com.linkedin.cdi.util.JsonParameter;
import com.linkedin.cdi.util.JsonUtils;
import com.linkedin.cdi.util.ParameterTypes;
import com.linkedin.cdi.util.SchemaBuilder;
import com.linkedin.cdi.util.SecretManager;
import com.linkedin.cdi.util.VariableUtils;
import com.linkedin.cdi.util.WorkUnitStatus;
import java.io.IOException;
Expand Down Expand Up @@ -378,7 +378,7 @@ List<StreamProcessor<?>> getPreprocessors(State state) {
for (Map.Entry<String, JsonElement> entry : preprocessorParams.entrySet()) {
String key = entry.getKey();
String value = preprocessorParams.get(key).getAsString();
String decryptedValue = EncryptionUtils.decryptGobblin(value, state);
String decryptedValue = SecretManager.getInstance(state).decrypt(value);
preprocessorParams.addProperty(key, decryptedValue);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import com.linkedin.cdi.factory.reader.SchemaReader;
import com.linkedin.cdi.factory.sftp.SftpChannelClient;
import com.linkedin.cdi.factory.sftp.SftpClient;
import com.linkedin.cdi.util.EncryptionUtils;
import com.linkedin.cdi.util.SecretManager;
import java.sql.Connection;
import java.sql.DriverManager;
import org.apache.gobblin.configuration.State;
Expand Down Expand Up @@ -63,9 +63,9 @@ public SdkHttpClient getS3Client(State state, AttributeMap config) {
public Connection getJdbcConnection(String jdbcUrl, String userId, String cryptedPassword, State state) {
try {
return DriverManager.getConnection(
EncryptionUtils.decryptGobblin(jdbcUrl, state),
EncryptionUtils.decryptGobblin(userId, state),
EncryptionUtils.decryptGobblin(cryptedPassword, state));
SecretManager.getInstance(state).decrypt(jdbcUrl),
SecretManager.getInstance(state).decrypt(userId),
SecretManager.getInstance(state).decrypt(cryptedPassword));
} catch (Exception e) {
LOG.error("Error creating JDBC connection", e);
throw new RuntimeException(e);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import com.linkedin.cdi.connection.HttpConnection;
import com.linkedin.cdi.extractor.MultistageExtractor;
import com.linkedin.cdi.keys.HttpKeys;
import com.linkedin.cdi.util.EncryptionUtils;
import com.linkedin.cdi.util.SecretManager;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
Expand Down Expand Up @@ -124,7 +124,7 @@ private JsonObject getRequestHeader(State state) {
JsonObject decrypted = new JsonObject();
for (Map.Entry<String, JsonElement> entry: headers.entrySet()) {
String key = entry.getKey();
decrypted.addProperty(key, EncryptionUtils.decryptGobblin(headers.get(key).getAsString(), state));
decrypted.addProperty(key, SecretManager.getInstance(state).decrypt(headers.get(key).getAsString()));
}
return decrypted;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Copyright 2021 LinkedIn Corporation. All rights reserved.
// Licensed under the BSD-2 Clause license.
// See LICENSE in the project root for license information.

package com.linkedin.cdi.util;

import org.apache.gobblin.configuration.State;


/**
* Interface for secret encryption and decryption
*/
public class GobblinSecretManager extends SecretManager {
public GobblinSecretManager(State state) {
super(state);
}

/**
* Decrypt the encrypted string
* @param input the encrypted string
* @return decrypted string
*/
@Override
public String decrypt(String input) {
return EncryptionUtils.decryptGobblin(input, state);
}

/**
* Encrypt the decrypted string
* @param input the unencrypted string
* @return encrypted string
*/
@Override
public String encrypt(String input) {
return EncryptionUtils.encryptGobblin(input, state);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@ private JsonObject selectFieldsFromGenericRecord(GenericRecord record, List<Stri
if (valueObject == null || fieldType == Schema.Type.NULL) {
jsonObject.add(field, JsonNull.INSTANCE);
} else if (fieldType == Schema.Type.STRING) {
jsonObject.addProperty(field, EncryptionUtils.decryptGobblin(valueObject.toString(), state));
jsonObject.addProperty(field, SecretManager.getInstance(state).decrypt(valueObject.toString()));
} else if (fieldType == Schema.Type.ARRAY) {
jsonObject.add(field, gson.fromJson(valueObject.toString(), JsonArray.class));
} else if (fieldType == Schema.Type.RECORD) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,9 +150,8 @@ private JsonObject parseParameter(JsonObject paramObject, JsonObject values) {
break;
case LIST:
// allow encryption on LIST type parameters
parsedObject.addProperty(name, EncryptionUtils.decryptGobblin(
parseListParameter(paramObject.get("value"), state),
state));
parsedObject.addProperty(name, SecretManager.getInstance(state).decrypt(
parseListParameter(paramObject.get("value"), state)));
break;

case JSONARRAY:
Expand Down
59 changes: 59 additions & 0 deletions cdi-core/src/main/java/com/linkedin/cdi/util/SecretManager.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// Copyright 2021 LinkedIn Corporation. All rights reserved.
// Licensed under the BSD-2 Clause license.
// See LICENSE in the project root for license information.

package com.linkedin.cdi.util;

import org.apache.gobblin.configuration.State;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import static com.linkedin.cdi.configuration.PropertyCollection.*;


/**
* Interface for secret encryption and decryption
*/
public abstract class SecretManager {
final private static Logger LOG = LoggerFactory.getLogger(SecretManager.class);
private static SecretManager manager = null;
protected State state;

public SecretManager(State state) {
this.state = state;
}
/**
* Decrypt the encrypted string
* @param input the encrypted string
* @return decrypted string
*/
abstract public String decrypt(String input);

/**
* Encrypt the decrypted string
* @param input the unencrypted string
* @return encrypted string
*/
abstract public String encrypt(String input);

static public SecretManager getInstance(State state) {
if (SecretManager.manager != null) {
return SecretManager.manager;
}

try {
Class<?> clazz = Class.forName(MSTAGE_SECRET_MANAGER_CLASS.get(state));
Object manager = clazz.getConstructor(State.class).newInstance(state);
if (manager instanceof SecretManager) {
SecretManager.manager = (SecretManager) manager;
}
} catch (RuntimeException re) {
throw re;
} catch (Exception e) {
LOG.error("Error creating required secret manager: {}", MSTAGE_SECRET_MANAGER_CLASS.get(state));
LOG.info("Returning default GobblinSecretManager.");
SecretManager.manager = new GobblinSecretManager(state);
}
return SecretManager.manager;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,19 +40,19 @@ public void setUp() {
@Test
void testDecryption() {
when(passwordManager.readPassword(ENC_PASSWORD)).thenReturn(PLAIN_PASSWORD);
Assert.assertEquals(EncryptionUtils.decryptGobblin(ENC_PASSWORD, state), PLAIN_PASSWORD);
Assert.assertEquals(EncryptionUtils.decryptGobblin(PLAIN_PASSWORD, state), PLAIN_PASSWORD);
Assert.assertEquals(SecretManager.getInstance(state).decrypt(ENC_PASSWORD), PLAIN_PASSWORD);
Assert.assertEquals(SecretManager.getInstance(state).decrypt(PLAIN_PASSWORD), PLAIN_PASSWORD);
}

@Test
void testEncryption() {
when(passwordManager.encryptPassword(PLAIN_PASSWORD)).thenReturn(ENC_PASSWORD);
when(passwordManager.readPassword(ENC_PASSWORD)).thenReturn(PLAIN_PASSWORD);
Assert.assertEquals(EncryptionUtils.decryptGobblin(EncryptionUtils.encryptGobblin(PLAIN_PASSWORD, state), state),
Assert.assertEquals(SecretManager.getInstance(state).decrypt(SecretManager.getInstance(state).encrypt(PLAIN_PASSWORD)),
PLAIN_PASSWORD);

when(passwordManager.encryptPassword(ENC_PASSWORD)).thenReturn(ENC_PASSWORD);
Assert.assertEquals(EncryptionUtils.decryptGobblin(EncryptionUtils.encryptGobblin(ENC_PASSWORD, state), state),
Assert.assertEquals(SecretManager.getInstance(state).decrypt(SecretManager.getInstance(state).encrypt(ENC_PASSWORD)),
PLAIN_PASSWORD);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,17 +43,6 @@ public void testJsonArrayParameters() {
Assert.assertEquals(results, JsonParameter.getParametersAsJsonString(jsonArray.toString(), new JsonObject(), new State()));
}

@Test
public void testParameterEncryption() {
String expected = "{\"test-parameter\":\"password\"}";
String encrypted = "ENC(M6nV+j0lhqZ36RgvuF5TQMyNvBtXmkPl)";
String masterKeyLoc = this.getClass().getResource("/key/master.key").toString();
SourceState state = new SourceState();
state.setProp(ENCRYPT_KEY_LOC.toString(), masterKeyLoc);
JsonArray jsonArray = gson.fromJson(new InputStreamReader(this.getClass().getResourceAsStream("/json/parameter-encryption.json")), JsonArray.class);
Assert.assertEquals(expected, JsonParameter.getParametersAsJsonString(jsonArray.toString(), new JsonObject(), state));
}

@Test
public void testJsonParameterConstructor() {
JsonObject object = new JsonObject();
Expand Down
25 changes: 25 additions & 0 deletions docs/parameters/ms.secret.manager.class.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# ms.secret.manager.class

**Tags**:

**Type**: string

**Default value**: `com.linkedin.cdi.util.GobblinSecretManager`

**Related**:

## Description

`ms.secret.manager.class` specifies the SecretManager class to use for secrets
and confidential data encryption and decryption.

Secrets include usernames, passwords, API keys, tokens, etc, that are essential for connections to other
data systems.

Confidential data include dataset columns that require encryption on storage.

Currently, we have the following SecretManager:

- `com.linkedin.cdi.util.GobblinSecretManager`

[back to summary](summary.md#mssecretmanagerclass)
5 changes: 5 additions & 0 deletions docs/parameters/summary.md
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,11 @@ on a pattern. By default, it will replace all blank spaces, $, and @ to undersco
Secondary inputs provides additional directives to job execution, in addition to
the primary inputs of job execution, which is its metadata, i.e, job configurations.

## [ms.secret.manager.class](ms.secret.manager.class.md)

`ms.secret.manager.class` specifies the SecretManager class to use for secrets
and confidential data encryption and decryption.

## [ms.session.key.field](ms.session.key.field.md)

Session is a state management mechanism over stateless connections.
Expand Down

0 comments on commit 7925f1a

Please sign in to comment.