Skip to content
Closed
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
6 changes: 5 additions & 1 deletion src/azure-cli-core/azure/cli/core/profiles/_shared.py
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,10 @@ def default_api_version(self):
ResourceType.MGMT_SERVICEBUS: '2022-10-01-preview',
ResourceType.MGMT_EVENTHUB: '2022-01-01-preview',
ResourceType.MGMT_MONITOR: None,
ResourceType.MGMT_MSI: '2023-01-31',
ResourceType.MGMT_MSI: SDKProfile('2023-01-31', {
'federated_identity_credentials': '2025-01-31-preview',
'user_assigned_identities': '2022-01-31-preview'
}),
ResourceType.MGMT_APPSERVICE: '2023-01-01',
ResourceType.MGMT_IOTHUB: '2023-06-30-preview',
ResourceType.MGMT_IOTDPS: '2021-10-15',
Expand Down Expand Up @@ -263,6 +266,7 @@ def default_api_version(self):
},
ResourceType.MGMT_MSI: {
'user_assigned_identities': '2022-01-31-preview',
'federated_identity_credentials': '2025-01-31-preview'
}
}

Expand Down
12 changes: 9 additions & 3 deletions src/azure-cli/azure/cli/command_modules/identity/_help.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,18 +45,24 @@
type: command
short-summary: Create a federated identity credential under an existing user assigned identity.
examples:
- name: Create a federated identity credential under a specific user assigned identity.
- name: Create a federated identity credential using subject.
text: |
az identity federated-credential create --name myFicName --identity-name myIdentityName --resource-group myResourceGroup --issuer myIssuer --subject mySubject --audiences myAudiences
- name: Create a federated identity credential using claims matching expression.
text: |
az identity federated-credential create --name myFicName --identity-name myIdentityName --resource-group myResourceGroup --issuer myIssuer --cme-value "expression" --cme-version 1 --audiences myAudiences
"""

helps['identity federated-credential update'] = """
type: command
short-summary: Update a federated identity credential under an existing user assigned identity.
examples:
- name: Update a federated identity credential under a specific user assigned identity.
- name: Update a federated identity credential using subject.
text: |
az identity federated-credential update --name myFicName --identity-name myIdentityName --resource-group myResourceGroup --issuer myIssuer --subject mySubject --audiences myAudiences
- name: Update a federated identity credential using claims matching expression.
text: |
az identity federated-credential update --name myFicName --identity-name myIdentityName --resource-group myResourceGroup --issuer myIssuer --cme-value "expression" --cme-version 1 --audiences myAudiences
"""

helps['identity federated-credential delete'] = """
Expand Down Expand Up @@ -84,4 +90,4 @@
- name: List all federated identity credentials under an existing user assigned identity.
text: |
az identity federated-credential list --identity-name myIdentityName --resource-group myResourceGroup
"""
"""
12 changes: 7 additions & 5 deletions src/azure-cli/azure/cli/command_modules/identity/_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,21 @@

def load_arguments(self, _):

with self.argument_context('identity') as c:
with self.argument_context('identity', operation_group='user_assigned_identities') as c:
c.argument('resource_name', arg_type=name_arg_type, id_part='name')

with self.argument_context('identity create') as c:
with self.argument_context('identity create', operation_group='user_assigned_identities') as c:
c.argument('location', get_location_type(self.cli_ctx), required=False)
c.argument('tags', tags_type)

with self.argument_context('identity federated-credential', min_api='2022-01-31-preview') as c:
with self.argument_context('identity federated-credential', operation_group='federated_identity_credentials') as c:
c.argument('federated_credential_name', options_list=('--name', '-n'), help='The name of the federated identity credential resource.')
c.argument('identity_name', help='The name of the identity resource.')

for scope in ['identity federated-credential create', 'identity federated-credential update']:
with self.argument_context(scope) as c:
with self.argument_context(scope, operation_group='federated_identity_credentials') as c:
c.argument('issuer', help='The openId connect metadata URL of the issuer of the identity provider that Azure AD would use in the token exchange protocol for validating tokens before issuing a token as the user-assigned managed identity.')
c.argument('subject', help='The sub value in the token sent to Azure AD for getting the user-assigned managed identity token. The value configured in the federated credential and the one in the incoming token must exactly match for Azure AD to issue the access token.')
c.argument('subject', help='The sub value in the token sent to Azure AD for getting the user-assigned managed identity token. The value configured in the federated credential and the one in the incoming token must exactly match for Azure AD to issue the access token. Cannot be used with --claims-matching-expression-value.')
c.argument('audiences', nargs='+', help='The aud value in the token sent to Azure for getting the user-assigned managed identity token. The value configured in the federated credential and the one in the incoming token must exactly match for Azure to issue the access token.')
c.argument('claims_matching_expression_value', options_list=['--cme-value'], help='The claims matching expression value that will be used to match against the subject claim in the token. When specified, --subject cannot be used.')
c.argument('claims_matching_expression_language_version', type=int, options_list=['--cme-version'], help='The language version of the claims matching expression.')
12 changes: 7 additions & 5 deletions src/azure-cli/azure/cli/command_modules/identity/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,18 @@ def load_command_table(self, _):

identity_sdk = CliCommandType(
operations_tmpl='azure.mgmt.msi.operations#UserAssignedIdentitiesOperations.{}',
client_factory=_msi_user_identities_operations
client_factory=_msi_user_identities_operations,
operation_group='user_assigned_identities'
)
msi_operations_sdk = CliCommandType(
operations_tmpl='azure.mgmt.msi.operations#Operations.{}',
client_factory=_msi_operations_operations
client_factory=_msi_operations_operations,
operation_group='operations'
)
federated_identity_credentials_sdk = CliCommandType(
operations_tmpl='azure.mgmt.msi.operations#FederatedIdentityCredentialsOperations.{}',
client_factory=_msi_federated_identity_credentials_operations
client_factory=_msi_federated_identity_credentials_operations,
operation_group='federated_identity_credentials'
)

with self.command_group('identity', identity_sdk, client_factory=_msi_user_identities_operations) as g:
Expand All @@ -38,8 +41,7 @@ def load_command_table(self, _):
g.command('list-operations', 'list')

with self.command_group('identity federated-credential', federated_identity_credentials_sdk,
client_factory=_msi_federated_identity_credentials_operations,
min_api='2022-01-31-preview') as g:
client_factory=_msi_federated_identity_credentials_operations) as g:
g.custom_command('create', 'create_or_update_federated_credential')
g.custom_command('update', 'create_or_update_federated_credential')
g.custom_show_command('show', 'show_federated_credential')
Expand Down
27 changes: 22 additions & 5 deletions src/azure-cli/azure/cli/command_modules/identity/custom.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,15 +35,32 @@ def list_identity_resources(cmd, resource_group_name, resource_name):


def create_or_update_federated_credential(cmd, client, resource_group_name, identity_name, federated_credential_name,
issuer=None, subject=None, audiences=None):
issuer=None, subject=None, audiences=None, claims_matching_expression_value=None,
claims_matching_expression_language_version=None):
_default_audiences = ['api://AzureADTokenExchange']
audiences = _default_audiences if not audiences else audiences
if not issuer or not subject:
raise RequiredArgumentMissingError('usage error: please provide both --issuer and --subject parameters')


if not issuer:
raise RequiredArgumentMissingError('usage error: --issuer is required')
if subject and claims_matching_expression_value:
raise RequiredArgumentMissingError('usage error: --subject and --claims-matching-expression-value cannot be used together')
if not subject and not claims_matching_expression_value:
raise RequiredArgumentMissingError('usage error: --subject or --claims-matching-expression-value is required')
if claims_matching_expression_value and claims_matching_expression_language_version is None:
raise RequiredArgumentMissingError('usage error: --claims-matching-expression-language-version must be specified when using --claims-matching-expression-value')

FederatedIdentityCredential = cmd.get_models('FederatedIdentityCredential', resource_type=ResourceType.MGMT_MSI,
operation_group='federated_identity_credentials')
parameters = FederatedIdentityCredential(issuer=issuer, subject=subject, audiences=audiences)

parameters = FederatedIdentityCredential(
issuer=issuer,
subject=subject if subject else None,
audiences=audiences,
claimsMatchingExpression={
'value': claims_matching_expression_value,
'languageVersion': claims_matching_expression_language_version
} if claims_matching_expression_value else None
)

return client.create_or_update(resource_group_name=resource_group_name, resource_name=identity_name,
federated_identity_credential_resource_name=federated_credential_name,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,14 @@ def test_federated_identity_credential(self, resource_group):
'identity': 'ide',
'fic1': 'fic1',
'fic2': 'fic2',
'fic3': 'fic3',
'subject1': 'system:serviceaccount:ns:svcaccount1',
'subject2': 'system:serviceaccount:ns:svcaccount2',
'subject3': 'system:serviceaccount:ns:svcaccount3',
'issuer': 'https://oidc.prod-aks.azure.com/IssuerGUID',
'audience': 'api://AzureADTokenExchange',
'cme_value': 'value.matches(\'test\')',
'cme_version': '1'
})

self.cmd('identity create -n {identity} -g {rg}')
Expand Down Expand Up @@ -118,3 +121,56 @@ def test_federated_identity_credential(self, resource_group):
self.check('type(@)', 'array'),
self.check('length(@)', 0)
])

@ResourceGroupPreparer(name_prefix='cli_test_federated_identity_credential_cme_', location='eastus2euap')
def test_federated_identity_credential_claims_matching(self, resource_group):
self.kwargs.update({
'identity': 'ide',
'fic1': 'fic1',
'issuer': 'https://oidc.prod-aks.azure.com/IssuerGUID',
'audience': 'api://AzureADTokenExchange',
'cme_value': 'value.matches(\'test\')',
'cme_version': '1',
'subject': 'system:serviceaccount:ns:svcaccount1'
})

self.cmd('identity create -n {identity} -g {rg}')

# create a federated identity credential with claims matching expression
self.cmd('identity federated-credential create --name {fic1} --identity-name {identity} --resource-group {rg} '
'--issuer {issuer} --audiences {audience} --cme-value {cme_value} --cme-version {cme_version}',
checks=[
self.check('length(audiences)', 1),
self.check('audiences[0]', '{audience}'),
self.check('issuer', '{issuer}'),
self.check('claimsMatchingExpression.value', '{cme_value}'),
self.check('claimsMatchingExpression.languageVersion', '{cme_version}')
])

# update to use subject instead of claims matching expression
self.cmd('identity federated-credential update --name {fic1} --identity-name {identity} --resource-group {rg} '
'--issuer {issuer} --audiences {audience} --subject {subject}',
checks=[
self.check('subject', '{subject}'),
self.check('claimsMatchingExpression', None)
])

# update back to claims matching expression
self.cmd('identity federated-credential update --name {fic1} --identity-name {identity} --resource-group {rg} '
'--issuer {issuer} --audiences {audience} --cme-value {cme_value} --cme-version {cme_version}',
checks=[
self.check('claimsMatchingExpression.value', '{cme_value}'),
self.check('claimsMatchingExpression.languageVersion', '{cme_version}'),
self.check('subject', None)
])

def test_federated_identity_credential_validation(self):
# Test missing claims matching expression version
with self.assertRaisesRegex(
Exception, '--claims-matching-expression-language-version must be specified when using --claims-matching-expression-value'):
self.cmd('identity federated-credential create -g rg1 --identity-name testid --name testfic --issuer https://test.com --cme-value "test"')

# Test version without value
with self.assertRaisesRegex(
Exception, '--subject or --claims-matching-expression-value is required'):
self.cmd('identity federated-credential create -g rg1 --identity-name testid --name testfic --issuer https://test.com --cme-version 1')
Loading