fix: don't fail the whole service list when one connection can't be decrypted - #31888
fix: don't fail the whole service list when one connection can't be decrypted#31888oluies wants to merge 1 commit into
Conversation
…ecrypted
decryptOrNullify(SecurityContext, ResultList<T>) decrypted each service in a
forEach with no error handling, so a single connection that could not be
decrypted propagated out of the loop and failed the entire list request. Every
other service disappeared from the UI along with the affected one, and because
rendering the list is what failed there was no way to reach the broken
service's edit form to repair its credentials.
The class already declares an abstract nullifyConnection(T) hook, implemented
by all eleven service resources, but nothing in the main source tree ever
called it -- the only call site was a unit test. Wire it up, which is what the
method's own name ("decryptOrNullify") implies was intended: catch per service,
log, and return that service without a connection.
The single-service GET /{id} path is left as-is, so the error is still raised
where it is actionable.
Fixes open-metadata#31887
❌ PR checklist incompleteThis PR cannot be merged until the following are addressed on its linked issue:
The fields live on the linked issue in the Shipping project (open the issue → right sidebar → Projects). After you set them, re-run this check (or push a commit) — issue/project changes do not re-trigger it automatically. Maintainers can bypass this check by adding the |
|
Hi there 👋 Thanks for your contribution! The OpenMetadata team will review the PR shortly! Once it has been labeled as Let us know if you need any help! |
| "Failed to decrypt connection of service '{}'; returning it without one: {}", | ||
| service.getFullyQualifiedName(), | ||
| e.getMessage()); | ||
| nullifyConnection(service); |
There was a problem hiding this comment.
💡 Quality: nullifyConnection relies on discarded return value's side effect
In the catch block, nullifyConnection(service) is called but its return value is discarded; the connection is only cleared because the generated withConnection(null) happens to mutate in place (currently true given generateBuilders=true without useInnerClassBuilders). If the codegen config ever switches to immutable/inner-class builders, withConnection would return a new instance and the original (undecryptable/encrypted) connection config would remain in the list and be serialized to the client, defeating the null-out. Assign the result back to the list to remove the hidden dependency, e.g. iterate by index and call data.set(i, nullifyConnection(service)).
Assign the nullified service back into the list instead of relying on in-place mutation.:
List<T> data = listOrEmpty(services.getData());
for (int i = 0; i < data.size(); i++) {
T service = data.get(i);
try {
decryptOrNullify(securityContext, service);
} catch (Exception e) {
LOG.warn(
"Failed to decrypt connection of service '{}'; returning it without one: {}",
service.getFullyQualifiedName(),
e.getMessage());
data.set(i, nullifyConnection(service));
}
}
return services;
- Apply fix
Check the box to apply the fix or reply for a change | Was this helpful? React with 👍 / 👎
Code Review 👍 Approved with suggestions 0 resolved / 1 findingsPrevents decryption failures on individual services from breaking the entire service list response. Consider assigning the return value of nullifyConnection(service) rather than relying on in-place mutation side effects. 💡 Quality: nullifyConnection relies on discarded return value's side effectIn the catch block, Assign the nullified service back into the list instead of relying on in-place mutation.🤖 Prompt for agentsOptionsDisplay: compact → Showing less information. Comment with these commands to change the behavior for this request:
Was this helpful? React with 👍 / 👎 | Powered by Gitar — free for open source |
| LOG.warn( | ||
| "Failed to decrypt connection of service '{}'; returning it without one: {}", | ||
| service.getFullyQualifiedName(), | ||
| e.getMessage()); |
There was a problem hiding this comment.
Decryption warning exposes credentials
When field-by-field decryption fails after processing secret fields, e.getMessage() contains the generated connection object's string representation, causing password or private-key material to be written to application logs. Log the service identity without the exception message.
How this was verified: The decryption failure message embeds toDecryptObject.toString(), whose generated representation includes password fields, and this warning logs that message unchanged.
| LOG.warn( | |
| "Failed to decrypt connection of service '{}'; returning it without one: {}", | |
| service.getFullyQualifiedName(), | |
| e.getMessage()); | |
| LOG.warn( | |
| "Failed to decrypt connection of service '{}'; returning it without one", | |
| service.getFullyQualifiedName()); |
Knowledge Base Used: Auth and Security: Authentication, Authorization, Secrets, and SCIM
Fixes #31887
Problem
ServiceEntityResource.decryptOrNullify(SecurityContext, ResultList<T>)decrypted each service inside aforEachwith no error handling:decryptOrNullify(securityContext, service)→retrieveServiceConnectionConfig→SecretsManager.decryptServiceConnectionConfig, which throws when a config cannot be decrypted. The exception propagates out of the loop and fails the wholelistrequest.The practical effect is that one service with an undecryptable connection — after a Fernet key change, for instance — empties the entire service list in the UI. Every other service disappears with it, and since rendering the list is what fails, there is no way to reach the broken service's edit form to repair its credentials. Recovery means editing
dbservice_entity.jsonin the metadata database or restoring the previous key.Because the code is in the shared base class, this applies to every service type.
Fix
The class already declares an abstract hook:
implemented by all eleven service resources (
ApiService,DashboardService,DatabaseService,DriveService,LLMService,McpService,MessagingService,MetadataService,MlModelService, …), typically asreturn service.withConnection(null);.Nothing in the main source tree ever called it — the only call site in the repository is a unit test (
McpServiceResourceTest:107). This PR wires it up, which is what the method's own name ("decryptOrNullify") implies was intended: catch per service, log a warning, and return that service without a connection.The single-service
GET /{id}path is deliberately left throwing, so the error stays visible where it is actionable and where the user is looking at exactly the entity that is broken.Notes for reviewers
@Slf4jadded to the class; it had no logger.forEachrelies onnullifyConnectionmutating the element in place. That holds:openmetadata-spec/pom.xmlsets<generateBuilders>true</generateBuilders>withoutuseInnerClassBuilders, so jsonschema2pojo emits fluent setters that assign tothisand returnthis. Happy to switch to an index-basedList.setif you would rather not depend on that.Exceptionrather than the specific type is deliberate: the throw sites wrap several causes (InvalidServiceConnectionException,SecretsManagerException, and whatever an external secrets manager raises), and the intent is that no single service can take down the list.Testing
I was not able to build locally — this was developed against a source checkout without the Maven toolchain, so the change is uncompiled and CI will be its first real check. What I did verify:
google-java-format(GOOGLE style) makes no changes to the file, and every line is within the 100-column limit, so the result is stable underreflowLongStrings=false.nullifyConnectionreally is uncalled, andwithConnectionreally does mutate, both checked against the tree as described above.Please push back if you would like a regression test added — a
ResultListcontaining one service whose config fails to decrypt, asserting the other entries still come back and the failed one has a null connection, would fit in the existing service resource tests. I did not add one blind, without being able to run the suite.Greptile Summary
This PR makes service-list connection decryption failures recover per entity, returning the affected service without its connection instead of failing the entire request.
Confidence Score: 3/5
This PR should not merge until the warning stops recording secret-bearing decryption exception messages.
Failed field-by-field decryption can leave plaintext values in the temporary connection object, whose generated string representation is embedded in the newly logged exception message.
Files Needing Attention: openmetadata-service/src/main/java/org/openmetadata/service/resources/services/ServiceEntityResource.java
Security Review
The new warning can log generated connection-object contents from decryption exceptions. Because password fields may already have been decrypted before a later field fails, this can disclose plaintext credentials to application logs.
Important Files Changed
Sequence Diagram
sequenceDiagram participant Client participant Resource as ServiceEntityResource participant Secrets as SecretsManager participant Logs Client->>Resource: List services loop Each service Resource->>Secrets: Decrypt connection fields Secrets-->>Secrets: Decrypt fields in place Secrets--xResource: Later field throws Resource->>Logs: Warn with exception message containing connection.toString() Resource-->>Resource: Nullify returned connection end Resource-->>Client: Service listReviews (1): Last reviewed commit: "fix: don't fail the whole service list w..." | Re-trigger Greptile
Context used: