mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-09-18 05:19:34 +00:00
Add delete secrets options for managed SM (#11972)
This commit is contained in:
parent
c3cec54be9
commit
79405080b3
@ -100,6 +100,14 @@ public abstract class ServiceEntityRepository<
|
|||||||
return service;
|
return service;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Remove the secrets from the secret manager */
|
||||||
|
@Override
|
||||||
|
protected void postDelete(T service) {
|
||||||
|
SecretsManagerFactory.getSecretsManager()
|
||||||
|
.deleteSecretsFromServiceConnectionConfig(
|
||||||
|
service.getConnection().getConfig(), service.getServiceType().value(), service.getName(), serviceType);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ServiceUpdater getUpdater(T original, T updated, Operation operation) {
|
public ServiceUpdater getUpdater(T original, T updated, Operation operation) {
|
||||||
return new ServiceUpdater(original, updated, operation);
|
return new ServiceUpdater(original, updated, operation);
|
||||||
|
@ -59,6 +59,12 @@ public class WorkflowRepository extends EntityRepository<Workflow> {
|
|||||||
entity.withOwner(owner).withOpenMetadataServerConnection(openmetadataConnection);
|
entity.withOwner(owner).withOpenMetadataServerConnection(openmetadataConnection);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Remove the secrets from the secret manager */
|
||||||
|
@Override
|
||||||
|
protected void postDelete(Workflow workflow) {
|
||||||
|
SecretsManagerFactory.getSecretsManager().deleteSecretsFromWorkflow(workflow);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void storeRelationships(Workflow entity) {
|
public void storeRelationships(Workflow entity) {
|
||||||
storeOwner(entity, entity.getOwner());
|
storeOwner(entity, entity.getOwner());
|
||||||
|
@ -19,6 +19,7 @@ import org.openmetadata.schema.security.secrets.SecretsManagerConfiguration;
|
|||||||
import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
|
import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
|
||||||
import software.amazon.awssdk.regions.Region;
|
import software.amazon.awssdk.regions.Region;
|
||||||
import software.amazon.awssdk.services.ssm.SsmClient;
|
import software.amazon.awssdk.services.ssm.SsmClient;
|
||||||
|
import software.amazon.awssdk.services.ssm.model.DeleteParameterRequest;
|
||||||
import software.amazon.awssdk.services.ssm.model.GetParameterRequest;
|
import software.amazon.awssdk.services.ssm.model.GetParameterRequest;
|
||||||
import software.amazon.awssdk.services.ssm.model.ParameterType;
|
import software.amazon.awssdk.services.ssm.model.ParameterType;
|
||||||
import software.amazon.awssdk.services.ssm.model.PutParameterRequest;
|
import software.amazon.awssdk.services.ssm.model.PutParameterRequest;
|
||||||
@ -72,6 +73,12 @@ public class AWSSSMSecretsManager extends AWSBasedSecretsManager {
|
|||||||
return ssmClient.getParameter(parameterRequest).parameter().value();
|
return ssmClient.getParameter(parameterRequest).parameter().value();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void deleteSecretInternal(String secretName) {
|
||||||
|
DeleteParameterRequest deleteParameterRequest = DeleteParameterRequest.builder().name(secretName).build();
|
||||||
|
this.ssmClient.deleteParameter(deleteParameterRequest);
|
||||||
|
}
|
||||||
|
|
||||||
public static AWSSSMSecretsManager getInstance(SecretsManagerConfiguration config, String clusterPrefix) {
|
public static AWSSSMSecretsManager getInstance(SecretsManagerConfiguration config, String clusterPrefix) {
|
||||||
if (INSTANCE == null) INSTANCE = new AWSSSMSecretsManager(config, clusterPrefix);
|
if (INSTANCE == null) INSTANCE = new AWSSSMSecretsManager(config, clusterPrefix);
|
||||||
return INSTANCE;
|
return INSTANCE;
|
||||||
|
@ -22,6 +22,7 @@ import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
|
|||||||
import software.amazon.awssdk.regions.Region;
|
import software.amazon.awssdk.regions.Region;
|
||||||
import software.amazon.awssdk.services.secretsmanager.SecretsManagerClient;
|
import software.amazon.awssdk.services.secretsmanager.SecretsManagerClient;
|
||||||
import software.amazon.awssdk.services.secretsmanager.model.CreateSecretRequest;
|
import software.amazon.awssdk.services.secretsmanager.model.CreateSecretRequest;
|
||||||
|
import software.amazon.awssdk.services.secretsmanager.model.DeleteSecretRequest;
|
||||||
import software.amazon.awssdk.services.secretsmanager.model.GetSecretValueRequest;
|
import software.amazon.awssdk.services.secretsmanager.model.GetSecretValueRequest;
|
||||||
import software.amazon.awssdk.services.secretsmanager.model.UpdateSecretRequest;
|
import software.amazon.awssdk.services.secretsmanager.model.UpdateSecretRequest;
|
||||||
|
|
||||||
@ -74,6 +75,12 @@ public class AWSSecretsManager extends AWSBasedSecretsManager {
|
|||||||
return this.secretsClient.getSecretValue(getSecretValueRequest).secretString();
|
return this.secretsClient.getSecretValue(getSecretValueRequest).secretString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void deleteSecretInternal(String secretName) {
|
||||||
|
DeleteSecretRequest deleteSecretRequest = DeleteSecretRequest.builder().secretId(secretName).build();
|
||||||
|
this.secretsClient.deleteSecret(deleteSecretRequest);
|
||||||
|
}
|
||||||
|
|
||||||
public static AWSSecretsManager getInstance(SecretsManagerConfiguration config, String clusterPrefix) {
|
public static AWSSecretsManager getInstance(SecretsManagerConfiguration config, String clusterPrefix) {
|
||||||
if (INSTANCE == null) INSTANCE = new AWSSecretsManager(config, clusterPrefix);
|
if (INSTANCE == null) INSTANCE = new AWSSecretsManager(config, clusterPrefix);
|
||||||
return INSTANCE;
|
return INSTANCE;
|
||||||
|
@ -45,6 +45,11 @@ public class InMemorySecretsManager extends ExternalSecretsManager {
|
|||||||
storeSecret(secretName, secretValue);
|
storeSecret(secretName, secretValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void deleteSecretInternal(String secretName) {
|
||||||
|
secretsMap.remove(secretName);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
String getSecret(String secretName) {
|
String getSecret(String secretName) {
|
||||||
String value = secretsMap.getOrDefault(secretName, null);
|
String value = secretsMap.getOrDefault(secretName, null);
|
||||||
|
@ -32,4 +32,8 @@ public class NoopSecretsManager extends SecretsManager {
|
|||||||
protected String storeValue(String fieldName, String value, String secretId, boolean store) {
|
protected String storeValue(String fieldName, String value, String secretId, boolean store) {
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Nothing to delete on the Noop SM. We only delete on External SM
|
||||||
|
@Override
|
||||||
|
protected void deleteSecretInternal(String secretName) {}
|
||||||
}
|
}
|
||||||
|
@ -301,4 +301,57 @@ public abstract class SecretsManager {
|
|||||||
void setFernet(Fernet fernet) {
|
void setFernet(Fernet fernet) {
|
||||||
this.fernet = fernet;
|
this.fernet = fernet;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected abstract void deleteSecretInternal(String secretName);
|
||||||
|
|
||||||
|
public void deleteSecretsFromServiceConnectionConfig(
|
||||||
|
Object connectionConfig, String connectionType, String connectionName, ServiceType serviceType) {
|
||||||
|
|
||||||
|
try {
|
||||||
|
Class<?> clazz = ReflectionUtil.createConnectionConfigClass(connectionType, serviceType);
|
||||||
|
Object newConnectionConfig = ClassConverterFactory.getConverter(clazz).convert(connectionConfig);
|
||||||
|
deleteSecrets(newConnectionConfig, buildSecretId(true, serviceType.value(), connectionName));
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
String message = SecretsUtil.buildExceptionMessageConnection(e.getMessage(), connectionType, true);
|
||||||
|
if (message != null) {
|
||||||
|
throw new InvalidServiceConnectionException(message);
|
||||||
|
}
|
||||||
|
throw InvalidServiceConnectionException.byMessage(
|
||||||
|
connectionType, String.format("Failed to delete secrets from connection instance of %s", connectionType));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void deleteSecretsFromWorkflow(Workflow workflow) {
|
||||||
|
Workflow workflowConverted = (Workflow) ClassConverterFactory.getConverter(Workflow.class).convert(workflow);
|
||||||
|
// we don't store OM conn sensitive data
|
||||||
|
workflowConverted.setOpenMetadataServerConnection(null);
|
||||||
|
try {
|
||||||
|
deleteSecrets(workflowConverted, buildSecretId(true, "workflow", workflow.getName()));
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new CustomExceptionMessage(
|
||||||
|
Response.Status.BAD_REQUEST,
|
||||||
|
String.format("Failed to delete secrets from workflow instance [%s]", workflow.getName()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void deleteSecrets(Object toDeleteSecretsFrom, String secretId) {
|
||||||
|
if (!DO_NOT_ENCRYPT_CLASSES.contains(toDeleteSecretsFrom.getClass())) {
|
||||||
|
Arrays.stream(toDeleteSecretsFrom.getClass().getMethods())
|
||||||
|
.filter(ReflectionUtil::isGetMethodOfObject)
|
||||||
|
.forEach(
|
||||||
|
method -> {
|
||||||
|
Object obj = ReflectionUtil.getObjectFromMethod(method, toDeleteSecretsFrom);
|
||||||
|
String fieldName = method.getName().replaceFirst("get", "");
|
||||||
|
// check if it has annotation:
|
||||||
|
// We are replicating the logic that we use for storing the fields we need to encrypt
|
||||||
|
// at encryptPasswordFields
|
||||||
|
if (obj != null && obj.getClass().getPackageName().startsWith("org.openmetadata")) {
|
||||||
|
deleteSecrets(obj, buildSecretId(false, secretId, fieldName.toLowerCase(Locale.ROOT)));
|
||||||
|
} else if (obj != null && method.getAnnotation(PasswordField.class) != null) {
|
||||||
|
deleteSecretInternal(buildSecretId(false, secretId, fieldName.toLowerCase(Locale.ROOT)));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -34,6 +34,17 @@ public class SecretsManagerFactory {
|
|||||||
case NOOP:
|
case NOOP:
|
||||||
case AWS_SSM:
|
case AWS_SSM:
|
||||||
case AWS:
|
case AWS:
|
||||||
|
/*
|
||||||
|
We handle AWS and AWS_SSM as a NoopSecretsManager since we don't
|
||||||
|
need to WRITE any secrets. We will be just reading them out of the
|
||||||
|
AWS instance on the INGESTION side, but the server does not need
|
||||||
|
to do anything here.
|
||||||
|
|
||||||
|
If for example we want to set the AWS SSM (non-managed) we configure
|
||||||
|
the server as `secretsManager: aws-ssm` and set the Airflow env vars
|
||||||
|
to connect to AWS SSM as specified in the docs:
|
||||||
|
https://docs.open-metadata.org/v1.0.0/deployment/secrets-manager/supported-implementations/aws-ssm-parameter-store
|
||||||
|
*/
|
||||||
secretsManager = NoopSecretsManager.getInstance(clusterName, secretsManagerProvider);
|
secretsManager = NoopSecretsManager.getInstance(clusterName, secretsManagerProvider);
|
||||||
break;
|
break;
|
||||||
case MANAGED_AWS:
|
case MANAGED_AWS:
|
||||||
|
@ -27,7 +27,7 @@ public final class IngestionPipelineBuilder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Build `IngestionPipeline` object with concrete class for the config which by definition it is a `Object`. */
|
/** Build `IngestionPipeline` object with concrete class for the config which by definition it is a `Object`. */
|
||||||
public static IngestionPipeline addDefinedConfig(IngestionPipeline ingestionPipeline) {
|
public static void addDefinedConfig(IngestionPipeline ingestionPipeline) {
|
||||||
if (DBT.equals(ingestionPipeline.getPipelineType()) && ingestionPipeline.getSourceConfig() != null) {
|
if (DBT.equals(ingestionPipeline.getPipelineType()) && ingestionPipeline.getSourceConfig() != null) {
|
||||||
ingestionPipeline
|
ingestionPipeline
|
||||||
.getSourceConfig()
|
.getSourceConfig()
|
||||||
@ -41,6 +41,5 @@ public final class IngestionPipelineBuilder {
|
|||||||
ClassConverterFactory.getConverter(OpenMetadataConnection.class)
|
ClassConverterFactory.getConverter(OpenMetadataConnection.class)
|
||||||
.convert(ingestionPipeline.getOpenMetadataServerConnection()));
|
.convert(ingestionPipeline.getOpenMetadataServerConnection()));
|
||||||
}
|
}
|
||||||
return ingestionPipeline;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,125 @@
|
|||||||
|
package org.openmetadata.service.secrets;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNotEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyString;
|
||||||
|
import static org.mockito.Mockito.lenient;
|
||||||
|
import static org.openmetadata.schema.api.services.CreateDatabaseService.DatabaseServiceType.Mysql;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import org.junit.jupiter.api.BeforeAll;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.Mockito;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import org.openmetadata.schema.api.services.DatabaseConnection;
|
||||||
|
import org.openmetadata.schema.entity.automations.TestServiceConnectionRequest;
|
||||||
|
import org.openmetadata.schema.entity.automations.Workflow;
|
||||||
|
import org.openmetadata.schema.entity.automations.WorkflowType;
|
||||||
|
import org.openmetadata.schema.entity.services.ServiceType;
|
||||||
|
import org.openmetadata.schema.services.connections.database.MysqlConnection;
|
||||||
|
import org.openmetadata.service.exception.SecretsManagerException;
|
||||||
|
import org.openmetadata.service.fernet.Fernet;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
public class SecretsManagerLifecycleTest {
|
||||||
|
|
||||||
|
private static final String ENCRYPTED_VALUE = "fernet:abcdef";
|
||||||
|
private static final String DECRYPTED_VALUE = "123456";
|
||||||
|
|
||||||
|
// We'll test the secret creation and deletion using the In Memory SM
|
||||||
|
private static InMemorySecretsManager secretsManager;
|
||||||
|
|
||||||
|
@BeforeAll
|
||||||
|
static void setUp() {
|
||||||
|
secretsManager = InMemorySecretsManager.getInstance("openmetadata");
|
||||||
|
Fernet fernet = Mockito.mock(Fernet.class);
|
||||||
|
lenient().when(fernet.decrypt(anyString())).thenReturn(DECRYPTED_VALUE);
|
||||||
|
lenient().when(fernet.decryptIfApplies(anyString())).thenReturn(DECRYPTED_VALUE);
|
||||||
|
lenient().when(fernet.encrypt(anyString())).thenReturn(ENCRYPTED_VALUE);
|
||||||
|
secretsManager.setFernet(fernet);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testDatabaseServiceConnectionConfigLifecycle() {
|
||||||
|
String password = "openmetadata-test";
|
||||||
|
String secretName = "/openmetadata/database/test/password";
|
||||||
|
String connectionName = "test";
|
||||||
|
Map<String, String> mysqlConnection = Map.of("password", password);
|
||||||
|
|
||||||
|
// Ensure encrypted service connection config encrypts the password
|
||||||
|
MysqlConnection actualConnection =
|
||||||
|
(MysqlConnection)
|
||||||
|
secretsManager.encryptServiceConnectionConfig(
|
||||||
|
mysqlConnection, Mysql.value(), connectionName, ServiceType.DATABASE);
|
||||||
|
assertNotEquals(password, actualConnection.getPassword());
|
||||||
|
|
||||||
|
// Decrypt the encrypted password and validate
|
||||||
|
actualConnection =
|
||||||
|
(MysqlConnection)
|
||||||
|
secretsManager.decryptServiceConnectionConfig(actualConnection, Mysql.value(), ServiceType.DATABASE);
|
||||||
|
assertEquals(DECRYPTED_VALUE, actualConnection.getPassword());
|
||||||
|
|
||||||
|
// SM will have the key stored
|
||||||
|
String secretValue = secretsManager.getSecret(secretName);
|
||||||
|
assertEquals(secretValue, DECRYPTED_VALUE);
|
||||||
|
|
||||||
|
// Now we delete the service
|
||||||
|
secretsManager.deleteSecretsFromServiceConnectionConfig(
|
||||||
|
mysqlConnection, "Mysql", connectionName, ServiceType.DATABASE);
|
||||||
|
|
||||||
|
// We won't be able to get the key again
|
||||||
|
SecretsManagerException exception =
|
||||||
|
assertThrows(SecretsManagerException.class, () -> secretsManager.getSecret(secretName));
|
||||||
|
|
||||||
|
assertEquals(exception.getMessage(), String.format("Key [%s] not found in in-memory secrets manager", secretName));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testWorkflowLifecycle() {
|
||||||
|
String password = "openmetadata_password";
|
||||||
|
String secretName = "/openmetadata/workflow/test-connection/request/connection/config/password";
|
||||||
|
|
||||||
|
Workflow workflow =
|
||||||
|
new Workflow()
|
||||||
|
.withName("test-connection")
|
||||||
|
.withWorkflowType(WorkflowType.TEST_CONNECTION)
|
||||||
|
.withRequest(
|
||||||
|
new TestServiceConnectionRequest()
|
||||||
|
.withServiceType(ServiceType.DATABASE)
|
||||||
|
.withConnectionType("Mysql")
|
||||||
|
.withConnection(
|
||||||
|
new DatabaseConnection()
|
||||||
|
.withConfig(
|
||||||
|
new MysqlConnection()
|
||||||
|
.withHostPort("mysql:3306")
|
||||||
|
.withUsername("openmetadata_user")
|
||||||
|
.withPassword(password))));
|
||||||
|
|
||||||
|
Workflow encrypted = secretsManager.encryptWorkflow(workflow);
|
||||||
|
TestServiceConnectionRequest encryptedRequest = (TestServiceConnectionRequest) encrypted.getRequest();
|
||||||
|
DatabaseConnection encryptedConnection = (DatabaseConnection) encryptedRequest.getConnection();
|
||||||
|
MysqlConnection encryptedConfig = (MysqlConnection) encryptedConnection.getConfig();
|
||||||
|
assertNotEquals(password, encryptedConfig.getPassword());
|
||||||
|
|
||||||
|
Workflow decrypted = secretsManager.decryptWorkflow(encrypted);
|
||||||
|
TestServiceConnectionRequest decryptedRequest = (TestServiceConnectionRequest) decrypted.getRequest();
|
||||||
|
DatabaseConnection decryptedConnection = (DatabaseConnection) decryptedRequest.getConnection();
|
||||||
|
MysqlConnection decryptedConfig = (MysqlConnection) decryptedConnection.getConfig();
|
||||||
|
assertEquals(DECRYPTED_VALUE, decryptedConfig.getPassword());
|
||||||
|
|
||||||
|
// SM will have the key stored
|
||||||
|
String secretValue = secretsManager.getSecret(secretName);
|
||||||
|
assertEquals(secretValue, DECRYPTED_VALUE);
|
||||||
|
|
||||||
|
// Now we delete the service
|
||||||
|
secretsManager.deleteSecretsFromWorkflow(workflow);
|
||||||
|
|
||||||
|
// We won't be able to get the key again
|
||||||
|
SecretsManagerException exception =
|
||||||
|
assertThrows(SecretsManagerException.class, () -> secretsManager.getSecret(secretName));
|
||||||
|
|
||||||
|
assertEquals(exception.getMessage(), String.format("Key [%s] not found in in-memory secrets manager", secretName));
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user