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;
|
||||
}
|
||||
|
||||
/** 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
|
||||
public ServiceUpdater getUpdater(T original, T updated, Operation operation) {
|
||||
return new ServiceUpdater(original, updated, operation);
|
||||
|
@ -59,6 +59,12 @@ public class WorkflowRepository extends EntityRepository<Workflow> {
|
||||
entity.withOwner(owner).withOpenMetadataServerConnection(openmetadataConnection);
|
||||
}
|
||||
|
||||
/** Remove the secrets from the secret manager */
|
||||
@Override
|
||||
protected void postDelete(Workflow workflow) {
|
||||
SecretsManagerFactory.getSecretsManager().deleteSecretsFromWorkflow(workflow);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void storeRelationships(Workflow entity) {
|
||||
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.regions.Region;
|
||||
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.ParameterType;
|
||||
import software.amazon.awssdk.services.ssm.model.PutParameterRequest;
|
||||
@ -72,6 +73,12 @@ public class AWSSSMSecretsManager extends AWSBasedSecretsManager {
|
||||
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) {
|
||||
if (INSTANCE == null) INSTANCE = new AWSSSMSecretsManager(config, clusterPrefix);
|
||||
return INSTANCE;
|
||||
|
@ -22,6 +22,7 @@ import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
|
||||
import software.amazon.awssdk.regions.Region;
|
||||
import software.amazon.awssdk.services.secretsmanager.SecretsManagerClient;
|
||||
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.UpdateSecretRequest;
|
||||
|
||||
@ -74,6 +75,12 @@ public class AWSSecretsManager extends AWSBasedSecretsManager {
|
||||
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) {
|
||||
if (INSTANCE == null) INSTANCE = new AWSSecretsManager(config, clusterPrefix);
|
||||
return INSTANCE;
|
||||
|
@ -45,6 +45,11 @@ public class InMemorySecretsManager extends ExternalSecretsManager {
|
||||
storeSecret(secretName, secretValue);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void deleteSecretInternal(String secretName) {
|
||||
secretsMap.remove(secretName);
|
||||
}
|
||||
|
||||
@Override
|
||||
String getSecret(String secretName) {
|
||||
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) {
|
||||
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) {
|
||||
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 AWS_SSM:
|
||||
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);
|
||||
break;
|
||||
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`. */
|
||||
public static IngestionPipeline addDefinedConfig(IngestionPipeline ingestionPipeline) {
|
||||
public static void addDefinedConfig(IngestionPipeline ingestionPipeline) {
|
||||
if (DBT.equals(ingestionPipeline.getPipelineType()) && ingestionPipeline.getSourceConfig() != null) {
|
||||
ingestionPipeline
|
||||
.getSourceConfig()
|
||||
@ -41,6 +41,5 @@ public final class IngestionPipelineBuilder {
|
||||
ClassConverterFactory.getConverter(OpenMetadataConnection.class)
|
||||
.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