Implement project pulling from Azure DevOps using Service Principals (#14628)

* Credential Lookup with multiple types
Allow looking up a credential with one of multiple type IDs.

* Allow Azure cred for SCM
Allow selecting an Azure Resource Manager credential for Git-based SCMs.
This is in order to enable using Azure Service Principals for project updates.

* Implement Azure Service Principal Git
This adds support for using an Azure Service Principal for project updates.

---------

Signed-off-by: Patrick Uiterwijk <patrick@puiterwijk.org>
This commit is contained in:
Patrick Uiterwijk 2024-03-07 22:07:03 +07:00 committed by GitHub
parent 727278aaa3
commit 2e2cd7f2de
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 49 additions and 9 deletions

View File

@ -160,8 +160,8 @@ class ProjectOptions(models.Model):
if self.scm_type == 'insights': if self.scm_type == 'insights':
if cred.kind != 'insights': if cred.kind != 'insights':
raise ValidationError(_("Credential kind must be 'insights'.")) raise ValidationError(_("Credential kind must be 'insights'."))
elif cred.kind != 'scm': elif cred.kind != 'scm' and cred.kind != 'azure_rm':
raise ValidationError(_("Credential kind must be 'scm'.")) raise ValidationError(_("Credential kind must be 'scm' or 'azure_rm'." % cred.kind))
try: try:
if self.scm_type == 'insights': if self.scm_type == 'insights':
self.scm_url = settings.INSIGHTS_URL_BASE self.scm_url = settings.INSIGHTS_URL_BASE

View File

@ -1239,6 +1239,9 @@ class RunProjectUpdate(BaseTask):
return scm_url, extra_vars return scm_url, extra_vars
def build_credentials_list(self, instance):
return [instance.credential]
def build_inventory(self, instance, private_data_dir): def build_inventory(self, instance, private_data_dir):
return 'localhost,' return 'localhost,'

View File

@ -38,6 +38,26 @@
tags: tags:
- update_git - update_git
block: block:
- name: Get Azure access token
when: "lookup('ansible.builtin.env', 'AZURE_CLIENT_ID') != ''"
register: azure_token
no_log: True
check_mode: false
azure.azcollection.azure_rm_accesstoken_info:
scopes:
# This is the audience for Azure DevOps, as per
# https://learn.microsoft.com/en-us/rest/api/azure/devops/tokens/
- 499b84ac-1321-427f-aa17-267ca6975798/.default
- name: Define git environment variables
when: "azure_token is not skipped"
no_log: True
ansible.builtin.set_fact:
git_environment:
GIT_CONFIG_COUNT: 1
GIT_CONFIG_KEY_0: http.extraHeader
GIT_CONFIG_VALUE_0: "Authorization: Bearer {{ azure_token.access_token }}"
- name: Update project using git - name: Update project using git
ansible.builtin.git: ansible.builtin.git:
dest: "{{ project_path | quote }}" dest: "{{ project_path | quote }}"
@ -47,6 +67,7 @@
force: "{{ scm_clean }}" force: "{{ scm_clean }}"
track_submodules: "{{ scm_track_submodules | default(omit) }}" track_submodules: "{{ scm_track_submodules | default(omit) }}"
accept_hostkey: "{{ scm_accept_hostkey | default(omit) }}" accept_hostkey: "{{ scm_accept_hostkey | default(omit) }}"
environment: "{{ git_environment | default({}) }}"
register: git_result register: git_result
- name: Set the git repository version - name: Set the git repository version

View File

@ -32,6 +32,7 @@ const QS_CONFIG = getQSConfig('credentials', {
function CredentialLookup({ function CredentialLookup({
autoPopulate, autoPopulate,
credentialTypeId, credentialTypeId,
credentialTypeIds,
credentialTypeKind, credentialTypeKind,
credentialTypeNamespace, credentialTypeNamespace,
fieldName, fieldName,
@ -61,6 +62,9 @@ function CredentialLookup({
const typeIdParams = credentialTypeId const typeIdParams = credentialTypeId
? { credential_type: credentialTypeId } ? { credential_type: credentialTypeId }
: {}; : {};
const typeIdsParams = credentialTypeIds
? { credential_type__in: credentialTypeIds.join() }
: {};
const typeKindParams = credentialTypeKind const typeKindParams = credentialTypeKind
? { credential_type__kind: credentialTypeKind } ? { credential_type__kind: credentialTypeKind }
: {}; : {};
@ -72,6 +76,7 @@ function CredentialLookup({
CredentialsAPI.read( CredentialsAPI.read(
mergeParams(params, { mergeParams(params, {
...typeIdParams, ...typeIdParams,
...typeIdsParams,
...typeKindParams, ...typeKindParams,
...typeNamespaceParams, ...typeNamespaceParams,
}) })
@ -101,6 +106,7 @@ function CredentialLookup({
autoPopulate, autoPopulate,
autoPopulateLookup, autoPopulateLookup,
credentialTypeId, credentialTypeId,
credentialTypeIds,
credentialTypeKind, credentialTypeKind,
credentialTypeNamespace, credentialTypeNamespace,
history.location.search, history.location.search,

View File

@ -33,6 +33,11 @@ const fetchCredentials = async (credential) => {
results: [scmCredentialType], results: [scmCredentialType],
}, },
}, },
{
data: {
results: [azurermCredentialType],
},
},
{ {
data: { data: {
results: [insightsCredentialType], results: [insightsCredentialType],
@ -45,13 +50,14 @@ const fetchCredentials = async (credential) => {
}, },
] = await Promise.all([ ] = await Promise.all([
CredentialTypesAPI.read({ kind: 'scm' }), CredentialTypesAPI.read({ kind: 'scm' }),
CredentialTypesAPI.read({ namespace: 'azure_rm' }),
CredentialTypesAPI.read({ name: 'Insights' }), CredentialTypesAPI.read({ name: 'Insights' }),
CredentialTypesAPI.read({ kind: 'cryptography' }), CredentialTypesAPI.read({ kind: 'cryptography' }),
]); ]);
if (!credential) { if (!credential) {
return { return {
scm: { typeId: scmCredentialType.id }, scm: { typeIds: [scmCredentialType.id, azurermCredentialType.id] },
insights: { typeId: insightsCredentialType.id }, insights: { typeId: insightsCredentialType.id },
cryptography: { typeId: cryptographyCredentialType.id }, cryptography: { typeId: cryptographyCredentialType.id },
}; };
@ -60,8 +66,12 @@ const fetchCredentials = async (credential) => {
const { credential_type_id } = credential; const { credential_type_id } = credential;
return { return {
scm: { scm: {
typeId: scmCredentialType.id, typeIds: [scmCredentialType.id, azurermCredentialType.id],
value: credential_type_id === scmCredentialType.id ? credential : null, value:
credential_type_id === scmCredentialType.id ||
credential_type_id === azurermCredentialType.id
? credential
: null,
}, },
insights: { insights: {
typeId: insightsCredentialType.id, typeId: insightsCredentialType.id,
@ -367,13 +377,13 @@ function ProjectForm({ project, submitError, ...props }) {
}); });
const [scmTypeOptions, setScmTypeOptions] = useState(null); const [scmTypeOptions, setScmTypeOptions] = useState(null);
const [credentials, setCredentials] = useState({ const [credentials, setCredentials] = useState({
scm: { typeId: null, value: null }, scm: { typeIds: null, value: null },
insights: { typeId: null, value: null }, insights: { typeId: null, value: null },
cryptography: { typeId: null, value: null }, cryptography: { typeId: null, value: null },
}); });
const [signatureValidationCredentials, setSignatureValidationCredentials] = const [signatureValidationCredentials, setSignatureValidationCredentials] =
useState({ useState({
scm: { typeId: null, value: null }, scm: { typeIds: null, value: null },
insights: { typeId: null, value: null }, insights: { typeId: null, value: null },
cryptography: { typeId: null, value: null }, cryptography: { typeId: null, value: null },
}); });

View File

@ -52,7 +52,7 @@ export const ScmCredentialFormField = ({
return ( return (
<CredentialLookup <CredentialLookup
credentialTypeId={credential.typeId} credentialTypeIds={credential.typeIds}
label={t`Source Control Credential`} label={t`Source Control Credential`}
value={credential.value} value={credential.value}
onChange={onCredentialChange} onChange={onCredentialChange}

View File

@ -50,7 +50,7 @@ class Project(HasCopy, HasCreate, HasNotifications, UnifiedJobTemplate):
def create_payload(self, name='', description='', scm_type='git', scm_url='', scm_branch='', organization=Organization, credential=None, **kwargs): def create_payload(self, name='', description='', scm_type='git', scm_url='', scm_branch='', organization=Organization, credential=None, **kwargs):
if credential: if credential:
if isinstance(credential, Credential): if isinstance(credential, Credential):
if credential.ds.credential_type.namespace not in ('scm', 'insights'): if credential.ds.credential_type.namespace not in ('scm', 'insights', 'azure_rm'):
credential = None # ignore incompatible credential from HasCreate dependency injection credential = None # ignore incompatible credential from HasCreate dependency injection
elif credential in (Credential,): elif credential in (Credential,):
credential = (Credential, dict(credential_type=(True, dict(kind='scm')))) credential = (Credential, dict(credential_type=(True, dict(kind='scm'))))