diff --git a/src/azure-cli-core/azure/cli/core/profiles/_shared.py b/src/azure-cli-core/azure/cli/core/profiles/_shared.py index 05bf41b7df9..1efbebac936 100644 --- a/src/azure-cli-core/azure/cli/core/profiles/_shared.py +++ b/src/azure-cli-core/azure/cli/core/profiles/_shared.py @@ -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', @@ -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' } } diff --git a/src/azure-cli/azure/cli/command_modules/identity/_help.py b/src/azure-cli/azure/cli/command_modules/identity/_help.py index 44949e01792..4ea3868627a 100644 --- a/src/azure-cli/azure/cli/command_modules/identity/_help.py +++ b/src/azure-cli/azure/cli/command_modules/identity/_help.py @@ -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'] = """ @@ -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 -""" +""" \ No newline at end of file diff --git a/src/azure-cli/azure/cli/command_modules/identity/_params.py b/src/azure-cli/azure/cli/command_modules/identity/_params.py index 9ff9aaee2bc..1ec83777879 100644 --- a/src/azure-cli/azure/cli/command_modules/identity/_params.py +++ b/src/azure-cli/azure/cli/command_modules/identity/_params.py @@ -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.') diff --git a/src/azure-cli/azure/cli/command_modules/identity/commands.py b/src/azure-cli/azure/cli/command_modules/identity/commands.py index d43da6df438..9c49bab0d89 100644 --- a/src/azure-cli/azure/cli/command_modules/identity/commands.py +++ b/src/azure-cli/azure/cli/command_modules/identity/commands.py @@ -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: @@ -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') diff --git a/src/azure-cli/azure/cli/command_modules/identity/custom.py b/src/azure-cli/azure/cli/command_modules/identity/custom.py index c1b80cb8848..c45b0509f13 100644 --- a/src/azure-cli/azure/cli/command_modules/identity/custom.py +++ b/src/azure-cli/azure/cli/command_modules/identity/custom.py @@ -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, diff --git a/src/azure-cli/azure/cli/command_modules/identity/tests/latest/test_identity.py b/src/azure-cli/azure/cli/command_modules/identity/tests/latest/test_identity.py index 9367a106f03..c3f326f8901 100644 --- a/src/azure-cli/azure/cli/command_modules/identity/tests/latest/test_identity.py +++ b/src/azure-cli/azure/cli/command_modules/identity/tests/latest/test_identity.py @@ -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}') @@ -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')