[WIP] Make githubapp private key storable in other credential sources#240
[WIP] Make githubapp private key storable in other credential sources#240Klaas- wants to merge 1 commit intoctrliq:mainfrom
Conversation
There was a problem hiding this comment.
Pull request overview
This PR expands credential input sourcing so that external credentials (e.g., Azure Key Vault) can themselves have fields populated from other credential sources, enabling secret-manager “chaining” (useful for storing GitHub App private keys or external-credential auth inputs in a separate secret store).
Changes:
- UI: Removes the special-case rendering for
credentialType.kind === 'external', so external credential fields now go through the standardCredentialPluginFieldflow. - API/Model: Allows
Credential.get_input()to resolve dynamic input sources for all credential kinds (includingexternal), and permits external credentials to be input-source targets. - API/Model: Resolves dynamic inputs on the source external credential when executing an input-source lookup, enabling multi-hop external credential resolution.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
awx/ui/src/screens/Credential/shared/CredentialFormFields/CredentialField.js |
Removes external-only field rendering to allow plugin-based sourcing UI for external credential fields. |
awx/main/models/credential/__init__.py |
Enables external→external input sourcing and resolves dynamic inputs on source credentials during backend lookups. |
Comments suppressed due to low confidence (1)
awx/ui/src/screens/Credential/shared/CredentialFormFields/CredentialField.js:226
- Removing the special-case UI for
credentialType.kind === 'external'means external credentials will now render insideCredentialPluginField, allowing form values likeinputs.<field>to become plugin objects ({credential, inputs}).ExternalTestModalcurrently builds the/credentials/:id/test/payload by copyingcredentialFormValues.inputs[field.id]verbatim, so it will send these objects for string fields and fail schema validation. The test modal (and/or test endpoint) needs to filter/serialize plugin-sourced fields similarly to save, or otherwise support resolving dynamic inputs when testing external credentials.
if (credentialType.kind === 'ssh' && fieldOptions.id === 'become_method') {
return (
<BecomeMethodField fieldOptions={fieldOptions} isRequired={isRequired} />
);
}
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| # Resolve any dynamic inputs on the source credential | ||
| # (e.g., a field sourced from another external credential like Azure KV) | ||
| for field_name in self.source_credential.dynamic_input_fields: | ||
| if field_name not in backend_kwargs: | ||
| backend_kwargs[field_name] = self.source_credential._get_dynamic_input(field_name) | ||
|
|
There was a problem hiding this comment.
Allowing external credentials to be targets (and resolving source_credential.dynamic_input_fields) enables external→external chaining, but there is no cycle detection. A 2-node loop (A sources from B, B sources from A) will recurse indefinitely when resolving inputs and can hang tasks or hit recursion limits. Add validation to prevent cycles (graph walk from source_credential through its input sources) and/or runtime protection (visited set / max depth) during dynamic resolution.
There was a problem hiding this comment.
This seems like a valid concern. Could easily fix with something like this (untested)
def clean_source_credential(self):
if self.source_credential.credential_type.kind != 'external':
raise ValidationError(_('Source must be an external credential'))
# Cycle detection: check if adding this link would create a cycle
visited = set()
def has_cycle(cred_id, target_id):
if cred_id == target_id:
return True
if cred_id in visited:
return False
visited.add(cred_id)
# Check all sources that point to this credential
for input_source in CredentialInputSource.objects.filter(target_credential_id=cred_id):
if has_cycle(input_source.source_credential_id, target_id):
return True
return False
if has_cycle(self.source_credential_id, self.target_credential_id):
raise ValidationError(_('This would create a circular dependency between credentials'))
return self.source_credential
| def clean_target_credential(self): | ||
| if self.target_credential.credential_type.kind == 'external': | ||
| raise ValidationError(_('Target must be a non-external credential')) | ||
| if self.target_credential == self.source_credential: | ||
| raise ValidationError(_('Target and source credentials must be different')) | ||
| return self.target_credential |
There was a problem hiding this comment.
This changes API validation semantics: CredentialInputSource.clean_target_credential no longer rejects external target credentials, so existing functional tests (e.g. test_create_credential_input_source_with_external_target_returns_400) and any clients relying on the old error will break. Please update/replace the test expectation and add coverage for the newly-supported external→external input sourcing behavior.
SUMMARY
I am not yet sure if this is a good idea :) But essentially this is a follow up to the github app PR, I would like to be able to use a azure keyvault (or other secret manager) secret to store the RSA key for the github app.
I am not really too familiar with the UI/API, while I tested it and it works I am not sure it's a good idea to remove the restrictions :) So I am looking for feedback on this change.
ISSUE TYPE
COMPONENT NAME
ASCENDER VERSION
ADDITIONAL INFORMATION