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
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package org.openmetadata.service.migration.mysql.v1130;

import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.openmetadata.service.migration.api.MigrationProcessImpl;
import org.openmetadata.service.migration.utils.MigrationFile;
import org.openmetadata.service.migration.utils.v1130.MigrationUtil;

@Slf4j
public class Migration extends MigrationProcessImpl {

public Migration(MigrationFile migrationFile) {
Expand All @@ -15,5 +17,13 @@ public Migration(MigrationFile migrationFile) {
@SneakyThrows
public void runDataMigration() {
MigrationUtil.updateOwnerChartFormulas();
try {
MigrationUtil.migrateWebhookSecretKeyToAuthType(handle);
} catch (Exception e) {
LOG.error(
"Failed to migrate webhook secretKey to authType in v1130 migration. "
+ "Webhook authentication may not work correctly until re-saved.",
e);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package org.openmetadata.service.migration.postgres.v1130;

import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.openmetadata.service.migration.api.MigrationProcessImpl;
import org.openmetadata.service.migration.utils.MigrationFile;
import org.openmetadata.service.migration.utils.v1130.MigrationUtil;

@Slf4j
public class Migration extends MigrationProcessImpl {

public Migration(MigrationFile migrationFile) {
Expand All @@ -15,5 +17,13 @@ public Migration(MigrationFile migrationFile) {
@SneakyThrows
public void runDataMigration() {
MigrationUtil.updateOwnerChartFormulas();
try {
MigrationUtil.migrateWebhookSecretKeyToAuthType(handle);
} catch (Exception e) {
LOG.error(
"Failed to migrate webhook secretKey to authType in v1130 migration. "
+ "Webhook authentication may not work correctly until re-saved.",
e);
Comment thread
yan-3005 marked this conversation as resolved.
}
}
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,26 @@
package org.openmetadata.service.migration.utils.v1130;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import java.util.List;
import java.util.Map;
import lombok.extern.slf4j.Slf4j;
import org.jdbi.v3.core.Handle;
import org.openmetadata.schema.dataInsight.custom.DataInsightCustomChart;
import org.openmetadata.schema.entity.events.SubscriptionDestination;
import org.openmetadata.schema.utils.JsonUtils;
import org.openmetadata.service.jdbi3.DataInsightSystemChartRepository;
import org.openmetadata.service.resources.databases.DatasourceConfig;
import org.openmetadata.service.util.EntityUtil;

@Slf4j
public class MigrationUtil {
private MigrationUtil() {}

private static final String UPDATE_MYSQL =
"UPDATE event_subscription_entity SET json = :json WHERE id = :id";
private static final String UPDATE_POSTGRES =
"UPDATE event_subscription_entity SET json = :json::jsonb WHERE id = :id";
private static final String OLD_FIELD = "owners.name.keyword";
private static final String NEW_FIELD = "ownerName";

Expand Down Expand Up @@ -48,4 +60,73 @@ public static void updateOwnerChartFormulas() {
}
}
}

public static void migrateWebhookSecretKeyToAuthType(Handle handle) {
LOG.info("Starting migration of webhook secretKey to authType");
List<Map<String, Object>> rows =
handle.createQuery("SELECT id, json FROM event_subscription_entity").mapToMap().list();
int migratedCount = 0;

for (Map<String, Object> row : rows) {
String id = row.get("id").toString();
String jsonStr = row.get("json").toString();

try {
ObjectNode root = (ObjectNode) JsonUtils.readTree(jsonStr);
JsonNode destinations = root.get("destinations");
if (destinations == null || !destinations.isArray()) {
continue;
}

boolean modified = false;
for (JsonNode destination : destinations) {
String type =
destination.get("type") != null
? destination.get("type").asText().toLowerCase()
: null;
if (!SubscriptionDestination.SubscriptionType.WEBHOOK
.value()
.toLowerCase()
.equals(type)) {
continue;
}

JsonNode config = destination.get("config");
if (config == null || !config.isObject()) {
continue;
}

JsonNode secretKeyNode = config.get("secretKey");
if (secretKeyNode == null
|| secretKeyNode.isNull()
|| secretKeyNode.asText().trim().isEmpty()) {
continue;
}

ObjectNode configObj = (ObjectNode) config;
ObjectNode bearerAuth =
JsonUtils.getObjectMapper()
.createObjectNode()
.put("type", "bearer")
.put("secretKey", secretKeyNode.asText());
configObj.set("authType", bearerAuth);
configObj.remove("secretKey");
modified = true;
Comment thread
yan-3005 marked this conversation as resolved.
}

if (modified) {
String updateSql =
Boolean.TRUE.equals(DatasourceConfig.getInstance().isMySQL())
? UPDATE_MYSQL
: UPDATE_POSTGRES;
handle.createUpdate(updateSql).bind("json", root.toString()).bind("id", id).execute();
migratedCount++;
}
} catch (Exception e) {
LOG.warn("Error migrating event subscription {}", id, e);
}
}

LOG.info("Migrated {} event subscriptions with secretKey to authType", migratedCount);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
package org.openmetadata.service.migration.utils.v1130;

import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.mockito.Answers.RETURNS_DEEP_STUBS;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import org.jdbi.v3.core.Handle;
import org.jdbi.v3.core.statement.Update;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.MockedStatic;
import org.openmetadata.service.resources.databases.DatasourceConfig;

class MigrationUtilTest {

private static final ObjectMapper MAPPER = new ObjectMapper();

private static final String WEBHOOK_WITH_SECRET_KEY =
"""
{
"destinations": [
{
"type": "Webhook",
"config": {
"endpoint": "https://example.com/hook",
"secretKey": "mysecret"
}
}
Comment thread
gitar-bot[bot] marked this conversation as resolved.
]
}
""";

private static final String WEBHOOK_WITHOUT_SECRET_KEY =
"""
{
"destinations": [
{
"type": "Webhook",
"config": {
"endpoint": "https://example.com/hook"
}
}
]
}
""";

private static final String WEBHOOK_WITH_EMPTY_SECRET_KEY =
"""
{
"destinations": [
{
"type": "Webhook",
"config": {
"endpoint": "https://example.com/hook",
"secretKey": ""
}
}
]
}
""";

private static final String WEBHOOK_ALREADY_MIGRATED =
"""
{
"destinations": [
{
"type": "Webhook",
"config": {
"endpoint": "https://example.com/hook",
"authType": { "type": "bearer", "secretKey": "mysecret" }
}
}
]
}
""";

private static final String NON_WEBHOOK_DESTINATION =
"""
{
"destinations": [
{
"type": "Slack",
"config": {
"secretKey": "mysecret"
}
}
]
}
""";

private Map<String, Object> row(String json) {
return Map.of("id", UUID.randomUUID().toString(), "json", json);
}

private Handle handleReturningRows(List<Map<String, Object>> rows) {
Handle handle = mock(Handle.class, RETURNS_DEEP_STUBS);
when(handle.createQuery(any(String.class)).mapToMap().list()).thenReturn(rows);
return handle;
}

private Handle handleWithUpdateCapture(List<Map<String, Object>> rows, Update mockUpdate) {
Handle handle = mock(Handle.class, RETURNS_DEEP_STUBS);
when(handle.createQuery(any(String.class)).mapToMap().list()).thenReturn(rows);
when(handle.createUpdate(any(String.class))).thenReturn(mockUpdate);
return handle;
}

@Test
void migrateWebhookSecretKeyToAuthTypeIsNoOpWhenNoRows() {
Handle handle = handleReturningRows(List.of());

assertDoesNotThrow(() -> MigrationUtil.migrateWebhookSecretKeyToAuthType(handle));

verify(handle, never()).createUpdate(any());
}

@Test
void migrateWebhookSecretKeyToAuthTypeIsNoOpWhenNoSecretKey() {
Handle handle = handleReturningRows(List.of(row(WEBHOOK_WITHOUT_SECRET_KEY)));

assertDoesNotThrow(() -> MigrationUtil.migrateWebhookSecretKeyToAuthType(handle));

verify(handle, never()).createUpdate(any());
}

@Test
void migrateWebhookSecretKeyToAuthTypeIsNoOpWhenEmptySecretKey() {
Handle handle = handleReturningRows(List.of(row(WEBHOOK_WITH_EMPTY_SECRET_KEY)));

assertDoesNotThrow(() -> MigrationUtil.migrateWebhookSecretKeyToAuthType(handle));

verify(handle, never()).createUpdate(any());
}

@Test
void migrateWebhookSecretKeyToAuthTypeIsNoOpWhenAlreadyMigrated() {
Handle handle = handleReturningRows(List.of(row(WEBHOOK_ALREADY_MIGRATED)));

assertDoesNotThrow(() -> MigrationUtil.migrateWebhookSecretKeyToAuthType(handle));

verify(handle, never()).createUpdate(any());
}

@Test
void migrateWebhookSecretKeyToAuthTypeIsNoOpForNonWebhookDestinations() {
Handle handle = handleReturningRows(List.of(row(NON_WEBHOOK_DESTINATION)));

assertDoesNotThrow(() -> MigrationUtil.migrateWebhookSecretKeyToAuthType(handle));

verify(handle, never()).createUpdate(any());
}

@Test
void migrateWebhookSecretKeyToAuthTypeMigratesMysql() throws Exception {
Update mockUpdate = mock(Update.class, RETURNS_DEEP_STUBS);
Handle handle = handleWithUpdateCapture(List.of(row(WEBHOOK_WITH_SECRET_KEY)), mockUpdate);

try (MockedStatic<DatasourceConfig> ds = mockStatic(DatasourceConfig.class)) {
DatasourceConfig mockConfig = mock(DatasourceConfig.class);
ds.when(DatasourceConfig::getInstance).thenReturn(mockConfig);
when(mockConfig.isMySQL()).thenReturn(true);

assertDoesNotThrow(() -> MigrationUtil.migrateWebhookSecretKeyToAuthType(handle));

ArgumentCaptor<String> sqlCaptor = ArgumentCaptor.forClass(String.class);
verify(handle).createUpdate(sqlCaptor.capture());
assertEquals(
"UPDATE event_subscription_entity SET json = :json WHERE id = :id", sqlCaptor.getValue());

ArgumentCaptor<String> jsonCaptor = ArgumentCaptor.forClass(String.class);
verify(mockUpdate).bind(eq("json"), jsonCaptor.capture());

JsonNode config =
MAPPER.readTree(jsonCaptor.getValue()).get("destinations").get(0).get("config");
assertNull(config.get("secretKey"));
assertNotNull(config.get("authType"));
assertEquals("bearer", config.get("authType").get("type").asText());
assertEquals("mysecret", config.get("authType").get("secretKey").asText());
}
}

@Test
void migrateWebhookSecretKeyToAuthTypeMigratesPostgres() throws Exception {
Update mockUpdate = mock(Update.class, RETURNS_DEEP_STUBS);
Handle handle = handleWithUpdateCapture(List.of(row(WEBHOOK_WITH_SECRET_KEY)), mockUpdate);

try (MockedStatic<DatasourceConfig> ds = mockStatic(DatasourceConfig.class)) {
DatasourceConfig mockConfig = mock(DatasourceConfig.class);
ds.when(DatasourceConfig::getInstance).thenReturn(mockConfig);
when(mockConfig.isMySQL()).thenReturn(false);

assertDoesNotThrow(() -> MigrationUtil.migrateWebhookSecretKeyToAuthType(handle));

ArgumentCaptor<String> sqlCaptor = ArgumentCaptor.forClass(String.class);
verify(handle).createUpdate(sqlCaptor.capture());
assertEquals(
"UPDATE event_subscription_entity SET json = :json::jsonb WHERE id = :id",
sqlCaptor.getValue());

ArgumentCaptor<String> jsonCaptor = ArgumentCaptor.forClass(String.class);
verify(mockUpdate).bind(eq("json"), jsonCaptor.capture());

JsonNode config =
MAPPER.readTree(jsonCaptor.getValue()).get("destinations").get(0).get("config");
assertNull(config.get("secretKey"));
assertNotNull(config.get("authType"));
assertEquals("bearer", config.get("authType").get("type").asText());
assertEquals("mysecret", config.get("authType").get("secretKey").asText());
}
}
}
Loading