Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
696034a
fix: allow removing optional password fields in connection edit form
miantalha45 Apr 21, 2026
becdc82
Merge branch 'main' into fix/password-field-remove-ux
miantalha45 Apr 21, 2026
40124af
Merge branch 'main' into fix/password-field-remove-ux
miantalha45 Apr 22, 2026
18c8973
Merge branch 'main' into fix/password-field-remove-ux
miantalha45 Apr 22, 2026
2afcea6
Merge branch 'main' into fix/password-field-remove-ux
miantalha45 Apr 22, 2026
36f7eee
fix delete functionality
miantalha45 Apr 22, 2026
c883780
Merge branch 'main' into fix/password-field-remove-ux
miantalha45 Apr 22, 2026
ecc19b7
address gitarbot suggestions
miantalha45 Apr 22, 2026
556054a
remove unused import
miantalha45 Apr 22, 2026
53e3ce1
Merge branch 'main' into fix/password-field-remove-ux
miantalha45 Apr 23, 2026
5ec7ed2
add e2e tests, change text from english to each locale
miantalha45 Apr 23, 2026
3e8999d
Merge branch 'main' into fix/password-field-remove-ux
miantalha45 Apr 23, 2026
c55e50c
Merge branch 'main' into fix/password-field-remove-ux
miantalha45 Apr 23, 2026
73aa83f
Merge branch 'main' into fix/password-field-remove-ux
miantalha45 Apr 23, 2026
af6388a
Merge branch 'main' into fix/password-field-remove-ux
miantalha45 Apr 24, 2026
badb6e1
Merge branch 'main' into fix/password-field-remove-ux
miantalha45 Apr 24, 2026
134325c
Merge branch 'main' into fix/password-field-remove-ux
miantalha45 Apr 24, 2026
0500c49
Merge branch 'main' into fix/password-field-remove-ux
miantalha45 Apr 25, 2026
6ba79e3
Merge branch 'main' into fix/password-field-remove-ux
miantalha45 Apr 25, 2026
c66ad90
Merge branch 'main' into fix/password-field-remove-ux
Rohit0301 Apr 26, 2026
2c87408
Merge branch 'main' into fix/password-field-remove-ux
miantalha45 Apr 27, 2026
b560118
Merge branch 'main' into fix/password-field-remove-ux
miantalha45 Apr 28, 2026
e15eb99
Merge branch 'main' into fix/password-field-remove-ux
miantalha45 May 2, 2026
07e032f
Fix UI Style
miantalha45 May 2, 2026
3001f1a
Merge branch 'main' into fix/password-field-remove-ux
miantalha45 May 2, 2026
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
Expand Up @@ -13,6 +13,7 @@

package org.openmetadata.service.secrets;

import java.util.Objects;
import org.openmetadata.schema.security.secrets.SecretsManagerProvider;

public class DBSecretsManager extends SecretsManager {
Expand All @@ -33,6 +34,9 @@ public static DBSecretsManager getInstance(

@Override
protected String storeValue(String fieldName, String value, String secretId, boolean store) {
if (Objects.isNull(value) || value.isEmpty()) {
return null;
}
return value;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,15 @@ protected ExternalSecretsManager(
@Override
protected String storeValue(String fieldName, String value, String secretId, boolean store) {
String fieldSecretId = buildSecretId(false, secretId, fieldName.toLowerCase(Locale.ROOT));
if (Objects.isNull(value) || value.isEmpty()) {
if (store) {
try {
deleteSecretInternal(fieldSecretId);
} catch (Exception e) {
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

storeValue swallows all exceptions when attempting to delete a secret (empty catch block). This can silently fail secret removal while the API returns success, leaving orphaned secrets and making troubleshooting difficult. Catch and ignore only expected "not found" cases; for other errors, at least log a warning (ideally rethrow to fail the update).

Suggested change
} catch (Exception e) {
} catch (Exception e) {
throw new UnhandledServerException(
String.format("Failed to delete secret '%s'", fieldSecretId), e);

Copilot uses AI. Check for mistakes.
}
}
Comment on lines +36 to +42
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

storeValue swallows all exceptions when attempting to delete a secret (empty catch block). This can hide real operational failures (permissions, network errors) and make secret deletion silently unreliable. At minimum, log the exception; ideally only ignore "not found"-style errors and surface others.

Copilot uses AI. Check for mistakes.
return null;
}
Comment thread
gitar-bot[bot] marked this conversation as resolved.
// check if value does not start with 'config:' only String can have password annotation
if (Boolean.FALSE.equals(isSecret(value))) {
if (store) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -414,9 +414,11 @@ private Object encryptPasswordFields(Object toEncryptObject, String secretId, bo
// set new value
ReflectionUtil.setValueInMethod(
toEncryptObject,
Fernet.isTokenized(newFieldValue)
? newFieldValue
: store ? fernet.encrypt(newFieldValue) : newFieldValue,
isNull(newFieldValue)
? null
: Fernet.isTokenized(newFieldValue)
? newFieldValue
: store ? fernet.encrypt(newFieldValue) : newFieldValue,
toSet);
}
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,9 @@ static void setUp() {
"openmetadata", "prefix", List.of("key:value", "key2:value2"), null));
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.decryptIfApplies(anyString()))
.thenAnswer(invocation -> invocation.getArgument(0));
lenient().when(fernet.encrypt(anyString())).thenReturn(ENCRYPTED_VALUE);
secretsManager.setFernet(fernet);
}
Expand Down Expand Up @@ -89,6 +91,21 @@ void testDecryptServiceConnectionWithoutPassword() {
assertNotSame(connection, actualConfig);
}

@Test
void testEncryptDatabaseConnectionWithEmptyPasswordRemovesValue() {
MysqlConnection connection =
new MysqlConnection().withAuthType(new basicAuth().withPassword(""));
Object actualConfig =
secretsManager.encryptServiceConnectionConfig(
connection, Mysql.value(), "test-empty-password", ServiceType.DATABASE);

assertEquals(
null,
JsonUtils.convertValue(((MysqlConnection) actualConfig).getAuthType(), basicAuth.class)
.getPassword());
assertNotSame(connection, actualConfig);
}

@Test
void testReturnsExpectedSecretManagerProvider() {
assertEquals(SecretsManagerProvider.DB, secretsManager.getSecretsManagerProvider());
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package org.openmetadata.service.secrets;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.openmetadata.schema.api.services.CreateDatabaseService.DatabaseServiceType.Mysql;
Expand Down Expand Up @@ -236,10 +235,9 @@ void testEncryptDatabaseConnectionWithEmptyPassword() {
mysqlConnection, Mysql.value(), "test-empty-password", ServiceType.DATABASE);

assertNotNull(actualConnection, "Encryption should succeed even with empty password");
assertFalse(
JsonUtils.convertValue(actualConnection.getAuthType(), basicAuth.class)
.getPassword()
.isEmpty(),
"Empty password should be converted to non-empty encrypted value");
assertEquals(
null,
JsonUtils.convertValue(actualConnection.getAuthType(), basicAuth.class).getPassword(),
"Empty password should be treated as explicit secret removal");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -261,27 +261,17 @@ void testBuildSecretIdHandlesSpecialCharacters() throws ApiException {
}

@Test
void testEmptySecretValueShouldBeStoredAsNullString() throws ApiException {
ArgumentCaptor<V1Secret> secretCaptor = ArgumentCaptor.forClass(V1Secret.class);
when(mockApiClient.readNamespacedSecret(anyString(), eq(NAMESPACE))).thenReturn(readRequest);
when(readRequest.execute()).thenThrow(new ApiException(404, "Not Found"));

when(mockApiClient.createNamespacedSecret(eq(NAMESPACE), any(V1Secret.class)))
.thenReturn(createRequest);
when(createRequest.execute()).thenReturn(new V1Secret());

secretsManager.storeValue("field", "", SECRET_ID, true);
void testEmptySecretValueShouldDeleteSecret() throws ApiException {
when(mockApiClient.deleteNamespacedSecret("openmetadata-database-myservice-field", NAMESPACE))
.thenReturn(deleteRequest);
when(deleteRequest.execute()).thenReturn(new V1Status());

verify(mockApiClient).createNamespacedSecret(eq(NAMESPACE), secretCaptor.capture());
verify(createRequest).execute();
String result = secretsManager.storeValue("field", "", SECRET_ID, true);

V1Secret createdSecret = secretCaptor.getValue();
Map<String, byte[]> data = createdSecret.getData();
assert data != null;
assertEquals(
ExternalSecretsManager.NULL_SECRET_STRING,
new String(data.get("value"), StandardCharsets.UTF_8),
"Empty string should be stored as 'null' to prevent secrets manager rejection");
assertNull(result);
verify(mockApiClient)
.deleteNamespacedSecret("openmetadata-database-myservice-field", NAMESPACE);
verify(deleteRequest).execute();
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -553,6 +553,70 @@ test.describe(
page.locator(String.raw`#root\/schemaRegistryTopicSuffixName`)
).toHaveValue('');
});

test('should clear optional saslPassword via remove button and persist the empty value', async ({
page,
}) => {
await visitServiceDetailsPage(
page,
{
name: kafkaService.entity.name,
type: SERVICE_TYPE.Messaging,
},
false,
false
);

await page.getByRole('tab', { name: 'Connection' }).click();
await page.getByTestId('edit-connection-button').click();
await waitForAllLoadersToDisappear(page);

const removePasswordButton = page.getByTestId(
'password-remove-btn-root/saslPassword'
);

await expect(removePasswordButton).toBeVisible();
await removePasswordButton.click();

const passwordInput = page.getByTestId(
'password-input-widget-root/saslPassword'
);

await expect(passwordInput).toBeVisible();
await expect(passwordInput).toHaveValue('');

const patchResponse = page.waitForResponse(
(response) =>
response.url().includes('/api/v1/services/messagingServices') &&
response.request().method() === 'PATCH'
);

await page.getByTestId('submit-btn').click();
await page.getByTestId('submit-btn').click();

const patch = await patchResponse;
const patchBody = patch.request().postDataJSON() as Array<{
op: string;
path: string;
value?: unknown;
}>;

const passwordOp = patchBody.find(
(op) => op.path === '/connection/config/saslPassword'
);

expect(passwordOp).toBeDefined();
expect(passwordOp?.value).toBe('');

await waitForAllLoadersToDisappear(page);

await page.getByTestId('edit-connection-button').click();
await waitForAllLoadersToDisappear(page);

await expect(
page.getByTestId('password-input-widget-root/saslPassword')
).toHaveValue('');
});
});
}
);
Loading
Loading