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..280639bde25 100644 --- a/src/azure-cli/azure/cli/command_modules/identity/_help.py +++ b/src/azure-cli/azure/cli/command_modules/identity/_help.py @@ -8,37 +8,40 @@ from knack.help_files import helps helps['identity'] = """ -type: group -short-summary: Managed Identities + type: group + short-summary: Managed Identities + long-summary: These commands are in preview and under development. Reference and support levels are not guaranteed, and these commands might be changed or removed in the future. """ helps['identity create'] = """ -type: command -short-summary: Create Identities. -examples: - - name: Create an identity. - text: | - az identity create --name MyIdentity --resource-group MyResourceGroup + type: command + short-summary: Create a user assigned managed identity. + long-summary: You can create a user assigned managed identity to use across multiple Azure resources. + examples: + - name: Create a user assigned managed identity. + text: | + az identity create --name MyIdentity --resource-group MyResourceGroup """ helps['identity list'] = """ -type: command -short-summary: List Managed Identities. + type: command + short-summary: List user assigned managed identities. """ helps['identity list-operations'] = """ -type: command -short-summary: List available operations for the Managed Identity provider. + type: command + short-summary: List operations for managed identities. """ helps['identity list-resources'] = """ -type: command -short-summary: List the associated resources for the identity. + type: command + short-summary: List resources associated with a managed identity. """ helps['identity federated-credential'] = """ -type: group -short-summary: Manage federated identity credentials under user assigned identities. + type: group + short-summary: Manage federated identity credentials for user assigned managed identities. + long-summary: These commands are in preview and use API version 2025-01-31-PREVIEW. """ helps['identity federated-credential create'] = """ @@ -48,40 +51,64 @@ - name: Create a federated identity credential under a specific user assigned identity. 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 with claims matching expressions. + text: | + az identity federated-credential create --name myFicName --identity-name myIdentityName --resource-group myResourceGroup --issuer https://tokens.githubusercontent.com --audiences api://AzureADTokenExchange --claims-matching-expression-value "claims['sub'] startswith 'repo:contoso-org/contoso-repo:ref:refs/heads'" --claims-matching-expression-version 1 """ 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. - text: | - az identity federated-credential update --name myFicName --identity-name myIdentityName --resource-group myResourceGroup --issuer myIssuer --subject mySubject --audiences myAudiences + 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. + 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 with claims matching expression for GitHub + text: | + az identity federated-credential update --name myFicName --identity-name myIdentityName --resource-group myResourceGroup --issuer https://token.actions.githubusercontent.com --audiences api://AzureADTokenExchange --claims-matching-expression-value "claims['sub'] startswith 'repo:contoso-org/contoso-repo:ref:refs/heads'" --claims-matching-expression-version 1 """ helps['identity federated-credential delete'] = """ -type: command -short-summary: Delete a federated identity credential under an existing user assigned identity. -examples: - - name: Delete a federated identity credential under a specific user assigned identity. - text: | - az identity federated-credential delete --name myFicName --identity-name myIdentityName --resource-group myResourceGroup + type: command + short-summary: Delete a federated identity credential. + examples: + - name: Delete a federated identity credential under a user assigned managed identity. + text: | + az identity federated-credential delete --name myFicName --identity-name myIdentityName --resource-group myResourceGroup """ helps['identity federated-credential show'] = """ -type: command -short-summary: Show a federated identity credential under an existing user assigned identity. -examples: - - name: Show a federated identity credential under a specific user assigned identity. - text: | - az identity federated-credential show --name myFicName --identity-name myIdentityName --resource-group myResourceGroup + type: command + short-summary: Show details of a federated identity credential. + examples: + - name: Show details of a federated identity credential under a user assigned managed identity. + text: | + az identity federated-credential show --name myFicName --identity-name myIdentityName --resource-group myResourceGroup +""" + +helps['identity show'] = """ + type: command + short-summary: Show details of a user assigned managed identity. + examples: + - name: Show details of a user assigned managed identity. + text: | + az identity show --name myIdentity --resource-group myResourceGroup +""" + +helps['identity delete'] = """ + type: command + short-summary: Delete a user assigned managed identity. + examples: + - name: Delete a user assigned managed identity. + text: | + az identity delete --name myIdentity --resource-group myResourceGroup """ helps['identity federated-credential list'] = """ -type: command -short-summary: List all federated identity credentials under an existing user assigned identity. -examples: - - name: List all federated identity credentials under an existing user assigned identity. - text: | - az identity federated-credential list --identity-name myIdentityName --resource-group myResourceGroup + type: command + short-summary: List all federated identity credentials under an existing user assigned identity. + examples: + - name: List all federated identity credentials under an existing user assigned identity. + text: | + az identity federated-credential list --identity-name myIdentityName --resource-group myResourceGroup """ 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..ec7604a1cb9 100644 --- a/src/azure-cli/azure/cli/command_modules/identity/_params.py +++ b/src/azure-cli/azure/cli/command_modules/identity/_params.py @@ -22,7 +22,7 @@ def load_arguments(self, _): 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', min_api='2025-01-31-PREVIEW') 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.') @@ -31,3 +31,5 @@ def load_arguments(self, _): 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('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=['--claims-matching-expression-value'], help='The claims expression value for the federated identity credential.') + c.argument('claims_matching_expression_version', options_list=['--claims-matching-expression-version'], help='The claims expression language version for the federated identity credential.') 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..5cc8cf08616 100644 --- a/src/azure-cli/azure/cli/command_modules/identity/commands.py +++ b/src/azure-cli/azure/cli/command_modules/identity/commands.py @@ -3,43 +3,47 @@ # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- - from azure.cli.core.commands import CliCommandType +from azure.cli.core.profiles import ResourceType -from ._client_factory import _msi_user_identities_operations, _msi_operations_operations, \ - _msi_federated_identity_credentials_operations - +from ._client_factory import (_msi_user_identities_operations, _msi_operations_operations, + _msi_federated_identity_credentials_operations) from ._validators import process_msi_namespace - 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, + resource_type=ResourceType.MGMT_MSI ) + msi_operations_sdk = CliCommandType( operations_tmpl='azure.mgmt.msi.operations#Operations.{}', - client_factory=_msi_operations_operations + client_factory=_msi_operations_operations, + resource_type=ResourceType.MGMT_MSI ) + 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, + resource_type=ResourceType.MGMT_MSI ) - with self.command_group('identity', identity_sdk, client_factory=_msi_user_identities_operations) as g: + # Base identity commands + with self.command_group('identity', identity_sdk, is_preview=True) as g: g.custom_command('create', 'create_identity', validator=process_msi_namespace) g.show_command('show', 'get') g.command('delete', 'delete') g.custom_command('list', 'list_user_assigned_identities') g.custom_command('list-resources', 'list_identity_resources', min_api='2021-09-30-preview') + g.command('list-operations', 'list', command_type=msi_operations_sdk) with self.command_group('identity', msi_operations_sdk, client_factory=_msi_operations_operations) as g: 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: + min_api='2025-01-31-PREVIEW') 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..704d3c2ad53 100644 --- a/src/azure-cli/azure/cli/command_modules/identity/custom.py +++ b/src/azure-cli/azure/cli/command_modules/identity/custom.py @@ -5,7 +5,8 @@ from azure.cli.core.profiles import ResourceType from azure.cli.core.azclierror import ( - RequiredArgumentMissingError + RequiredArgumentMissingError, + MutuallyExclusiveArgumentError ) @@ -35,15 +36,36 @@ 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_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: + recommendation = "Please provide the issuer URL using --issuer parameter. For GitHub Actions, use 'https://token.actions.githubusercontent.com'" + raise RequiredArgumentMissingError('Issuer URL is required for federated credential creation', recommendation) + + # When subject is present, CME should be null + if subject: + claims_matching_expression_value = None + claims_matching_expression_version = None + elif not claims_matching_expression_value: + recommendation = [ + "Option 1: Provide --subject to create a federated credential with direct subject matching", + "Option 2: Provide --claims-matching-expression-value and --claims-matching-expression-version to create a federated credential with claims matching expression" + ] + raise RequiredArgumentMissingError('Missing required authentication criteria for federated credential', recommendation) + + if subject and claims_matching_expression_value: + recommendation = [ + "Option 1: Remove --subject to use claims matching expression", + "Option 2: Remove --claims-matching-expression-value and --claims-matching-expression-version to use subject" + ] + raise MutuallyExclusiveArgumentError('Subject and claims matching expression cannot be used together', recommendation) 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, audiences=audiences, + claims_matching_expression_value=claims_matching_expression_value, + claims_matching_expression_version=claims_matching_expression_version) return client.create_or_update(resource_group_name=resource_group_name, resource_name=identity_name, federated_identity_credential_resource_name=federated_credential_name, @@ -61,4 +83,4 @@ def show_federated_credential(client, resource_group_name, identity_name, federa def list_federated_credential(client, resource_group_name, identity_name): - return client.list(resource_group_name=resource_group_name, resource_name=identity_name) + return client.list(resource_group_name=resource_group_name, resource_name=identity_name) \ No newline at end of file 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..4c4c2e6945f 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 @@ -44,7 +44,7 @@ def test_federated_identity_credential(self, resource_group): self.cmd('identity create -n {identity} -g {rg}') - # create a federated identity credential + # test federated identity credential with basic parameters self.cmd('identity federated-credential create --name {fic1} --identity-name {identity} --resource-group {rg} ' '--subject {subject1} --issuer {issuer} --audiences {audience}', checks=[ @@ -54,14 +54,21 @@ def test_federated_identity_credential(self, resource_group): self.check('subject', '{subject1}') ]) - # create a federated identity credential + # test federated identity credential with claims matching expression + self.kwargs.update({ + 'claims_expr': "claims['sub'] startswith 'repo:contoso-org/contoso-repo:ref:refs/heads'", + 'claims_ver': '1' + }) self.cmd('identity federated-credential create --name {fic2} --identity-name {identity} --resource-group {rg} ' - '--subject {subject2} --issuer {issuer} --audiences {audience}', + '--issuer https://token.actions.githubusercontent.com --audiences {audience} ' + '--claims-matching-expression-value "{claims_expr}" ' + '--claims-matching-expression-version {claims_ver}', checks=[ self.check('length(audiences)', 1), self.check('audiences[0]', '{audience}'), - self.check('issuer', '{issuer}'), - self.check('subject', '{subject2}') + self.check('issuer', 'https://token.actions.githubusercontent.com'), + self.check('claimsMatchingExpressionValue', '{claims_expr}'), + self.check('claimsMatchingExpressionVersion', '{claims_ver}') ]) # show the federated identity credential @@ -84,16 +91,21 @@ def test_federated_identity_credential(self, resource_group): self.check('[0].subject', '{subject1}'), self.check('length([1].audiences)', '1'), self.check('[1].audiences[0]', '{audience}'), - self.check('[1].issuer', '{issuer}'), - self.check('[1].subject', '{subject2}'), + self.check('[1].issuer', 'https://token.actions.githubusercontent.com'), + self.check('[1].claimsMatchingExpressionValue', '{claims_expr}'), + self.check('[1].claimsMatchingExpressionVersion', '{claims_ver}'), ]) - # update a federated identity credential + # update a federated identity credential with subject (should clear CME) self.cmd('identity federated-credential update --name {fic1} --identity-name {identity} --resource-group {rg} ' - '--subject {subject3} --issuer {issuer} --audiences {audience}', + '--issuer https://token.actions.githubusercontent.com --audiences {audience} ' + '--claims-matching-expression-value "{new_claims_expr}" ' + '--claims-matching-expression-version {claims_ver}', checks=[ self.check('name', '{fic1}'), - self.check('subject', '{subject3}') + self.check('subject', '{subject3}'), + self.check('claimsMatchingExpressionValue', None), + self.check('claimsMatchingExpressionVersion', None) ]) # delete a federated identity credential @@ -106,8 +118,9 @@ def test_federated_identity_credential(self, resource_group): self.check('[0].name', '{fic2}'), self.check('length([0].audiences)', '1'), self.check('[0].audiences[0]', '{audience}'), - self.check('[0].issuer', '{issuer}'), - self.check('[0].subject', '{subject2}'), + self.check('[0].issuer', 'https://token.actions.githubusercontent.com'), + self.check('[0].claimsMatchingExpressionValue', '{claims_expr}'), + self.check('[0].claimsMatchingExpressionVersion', '{claims_ver}'), ]) # delete a federated identity credential @@ -118,3 +131,66 @@ 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_cme(self, resource_group): + self.kwargs.update({ + 'identity': 'ide', + 'fic': 'fic1', + 'issuer': 'https://tokens.githubusercontent.com', + 'subject': None, + 'audience': 'api://AzureADTokenExchange', + 'expression_value': "claims['sub'] startswith 'repo:contoso-org/contoso-repo:ref:refs/heads'", + 'expression_version': '1', + 'new_expression_value': "claims['sub'] startswith 'repo:contoso-org/contoso-repo:environment:Production'" + }) + + self.cmd('identity create -n {identity} -g {rg}') + + # create a federated identity credential with CME + self.cmd('identity federated-credential create --name {fic} --identity-name {identity} --resource-group {rg} ' + '--issuer {issuer} --audiences {audience} ' + '--claims-matching-expression-value "{expression_value}" ' + '--claims-matching-expression-version {expression_version}', + checks=[ + self.check('length(audiences)', 1), + self.check('audiences[0]', '{audience}'), + self.check('issuer', '{issuer}'), + self.check('subject', None), + self.check('claimsMatchingExpressionValue', '{expression_value}'), + self.check('claimsMatchingExpressionVersion', '{expression_version}') + ]) + + # show the federated identity credential with CME + self.cmd('identity federated-credential show --name {fic} --identity-name {identity} --resource-group {rg}', + checks=[ + self.check('length(audiences)', 1), + self.check('audiences[0]', '{audience}'), + self.check('issuer', '{issuer}'), + self.check('subject', None), + self.check('claimsMatchingExpressionValue', '{expression_value}'), + self.check('claimsMatchingExpressionVersion', '{expression_version}') + ]) + + # update federated identity credential with new CME + self.cmd('identity federated-credential update --name {fic} --identity-name {identity} --resource-group {rg} ' + '--claims-matching-expression-value "{new_expression_value}"', + checks=[ + self.check('name', '{fic}'), + self.check('claimsMatchingExpressionValue', '{new_expression_value}'), + self.check('claimsMatchingExpressionVersion', '{expression_version}') + ]) + + # list the federated identity credentials and verify CME + self.cmd('identity federated-credential list --identity-name {identity} --resource-group {rg}', + checks=[ + self.check('type(@)', 'array'), + self.check('length(@)', 1), + self.check('[0].name', '{fic}'), + self.check('[0].claimsMatchingExpressionValue', '{new_expression_value}'), + self.check('[0].claimsMatchingExpressionVersion', '{expression_version}') + ]) + + # cleanup + self.cmd('identity federated-credential delete --name {fic} ' + '--identity-name {identity} --resource-group {rg} --yes')