From 823944765ce9841ff91a4c2ba944cc9f31b12f16 Mon Sep 17 00:00:00 2001 From: Srujan Bandarkar Date: Wed, 30 Apr 2025 15:29:55 -0400 Subject: [PATCH 1/6] [Identity] Add az identity federated-credential create/update: Add support for claims matching expressions with 2025-01-31-PREVIEW API version --- .../cli/command_modules/identity/_help.py | 5 +- .../cli/command_modules/identity/_params.py | 4 +- .../cli/command_modules/identity/commands.py | 4 +- .../cli/command_modules/identity/custom.py | 34 +++++++-- .../identity/tests/latest/test_identity.py | 69 ++++++++++++++++++- 5 files changed, 104 insertions(+), 12 deletions(-) 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..5877e184d12 100644 --- a/src/azure-cli/azure/cli/command_modules/identity/_help.py +++ b/src/azure-cli/azure/cli/command_modules/identity/_help.py @@ -48,6 +48,9 @@ - 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'] = """ @@ -84,4 +87,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..706bbc9efd4 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.') \ No newline at end of file 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..23bf9e5a74b 100644 --- a/src/azure-cli/azure/cli/command_modules/identity/commands.py +++ b/src/azure-cli/azure/cli/command_modules/identity/commands.py @@ -39,9 +39,9 @@ def load_command_table(self, _): 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') g.custom_command('delete', 'delete_federated_credential', confirmation=True) - g.custom_command('list', 'list_federated_credential') + g.custom_command('list', 'list_federated_credential') \ No newline at end of file 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..392e796ce33 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 @@ -88,12 +88,14 @@ def test_federated_identity_credential(self, resource_group): self.check('[1].subject', '{subject2}'), ]) - # 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}', 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 @@ -118,3 +120,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') \ No newline at end of file From 1a020f20886121906a9fdc16ee990959eb45777e Mon Sep 17 00:00:00 2001 From: Srujan Bandarkar Date: Wed, 30 Apr 2025 15:59:54 -0400 Subject: [PATCH 2/6] Add appropriate version gating to the help entries to indicate they are for a future preview version --- src/azure-cli/azure/cli/command_modules/identity/_help.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) 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 5877e184d12..bb23156f0a1 100644 --- a/src/azure-cli/azure/cli/command_modules/identity/_help.py +++ b/src/azure-cli/azure/cli/command_modules/identity/_help.py @@ -39,10 +39,12 @@ helps['identity federated-credential'] = """ type: group short-summary: Manage federated identity credentials under user assigned identities. +min_api: 2025-01-31-PREVIEW """ helps['identity federated-credential create'] = """ type: command +min_api: 2025-01-31-PREVIEW 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. @@ -54,6 +56,7 @@ """ helps['identity federated-credential update'] = """ +min_api: 2025-01-31-PREVIEW type: command short-summary: Update a federated identity credential under an existing user assigned identity. examples: @@ -63,6 +66,7 @@ """ helps['identity federated-credential delete'] = """ +min_api: 2025-01-31-PREVIEW type: command short-summary: Delete a federated identity credential under an existing user assigned identity. examples: @@ -72,6 +76,7 @@ """ helps['identity federated-credential show'] = """ +min_api: 2025-01-31-PREVIEW type: command short-summary: Show a federated identity credential under an existing user assigned identity. examples: @@ -81,10 +86,11 @@ """ helps['identity federated-credential list'] = """ +min_api: 2025-01-31-PREVIEW 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 -""" \ No newline at end of file +""" From 3760e38bc4e841bacff2592c62cf1e236ee87052 Mon Sep 17 00:00:00 2001 From: Srujan Bandarkar Date: Wed, 30 Apr 2025 16:07:40 -0400 Subject: [PATCH 3/6] Removing all example commands from the help entries since the linter validates examples regardless of API version --- .../cli/command_modules/identity/_help.py | 23 ------------------- 1 file changed, 23 deletions(-) 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 bb23156f0a1..bd7930028d3 100644 --- a/src/azure-cli/azure/cli/command_modules/identity/_help.py +++ b/src/azure-cli/azure/cli/command_modules/identity/_help.py @@ -46,51 +46,28 @@ type: command min_api: 2025-01-31-PREVIEW 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. - 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'] = """ min_api: 2025-01-31-PREVIEW 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 """ helps['identity federated-credential delete'] = """ min_api: 2025-01-31-PREVIEW 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 """ helps['identity federated-credential show'] = """ min_api: 2025-01-31-PREVIEW 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 """ helps['identity federated-credential list'] = """ min_api: 2025-01-31-PREVIEW 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 """ From 5dccf011a0a08a356a94bd9b9141acfc87b27c71 Mon Sep 17 00:00:00 2001 From: Srujan Bandarkar Date: Wed, 30 Apr 2025 22:37:29 -0400 Subject: [PATCH 4/6] fix linting --- .../cli/command_modules/identity/_help.py | 43 +++++++++--- .../cli/command_modules/identity/_params.py | 18 ++--- .../cli/command_modules/identity/commands.py | 5 +- .../cli/command_modules/identity/custom.py | 66 ++++++++++--------- 4 files changed, 80 insertions(+), 52 deletions(-) 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 bd7930028d3..6ed29dbd33f 100644 --- a/src/azure-cli/azure/cli/command_modules/identity/_help.py +++ b/src/azure-cli/azure/cli/command_modules/identity/_help.py @@ -38,36 +38,59 @@ helps['identity federated-credential'] = """ type: group -short-summary: Manage federated identity credentials under user assigned identities. +short-summary: "[Preview] Manage federated identity credentials under user assigned identities." min_api: 2025-01-31-PREVIEW """ helps['identity federated-credential create'] = """ type: command +short-summary: "[Preview] Create a federated identity credential under an existing user assigned identity." min_api: 2025-01-31-PREVIEW -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. + 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'] = """ -min_api: 2025-01-31-PREVIEW type: command -short-summary: Update a federated identity credential under an existing user assigned identity. +short-summary: "[Preview] Update a federated identity credential under an existing user assigned identity." +min_api: 2025-01-31-PREVIEW +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 """ helps['identity federated-credential delete'] = """ -min_api: 2025-01-31-PREVIEW type: command -short-summary: Delete a federated identity credential under an existing user assigned identity. +short-summary: "[Preview] Delete a federated identity credential under an existing user assigned identity." +min_api: 2025-01-31-PREVIEW +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 """ helps['identity federated-credential show'] = """ -min_api: 2025-01-31-PREVIEW type: command -short-summary: Show a federated identity credential under an existing user assigned identity. +short-summary: "[Preview] Show a federated identity credential under an existing user assigned identity." +min_api: 2025-01-31-PREVIEW +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 """ helps['identity federated-credential list'] = """ -min_api: 2025-01-31-PREVIEW type: command -short-summary: List all federated identity credentials under an existing user assigned identity. +short-summary: "[Preview] List all federated identity credentials under an existing user assigned identity." +min_api: 2025-01-31-PREVIEW +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 706bbc9efd4..6eb7f50e084 100644 --- a/src/azure-cli/azure/cli/command_modules/identity/_params.py +++ b/src/azure-cli/azure/cli/command_modules/identity/_params.py @@ -22,14 +22,14 @@ 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='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.') + with self.argument_context('identity federated-credential', min_api='2025-01-31-PREVIEW', is_preview=True) as c: + c.argument('federated_credential_name', options_list=('--name', '-n'), help='[Preview] The name of the federated identity credential resource.') + c.argument('identity_name', help='[Preview] The name of the identity resource.') for scope in ['identity federated-credential create', 'identity federated-credential update']: - with self.argument_context(scope) 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('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.') \ No newline at end of file + with self.argument_context(scope, is_preview=True) as c: + c.argument('issuer', help='[Preview] 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='[Preview] 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='[Preview] 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='[Preview] The claims expression value for the federated identity credential.') + c.argument('claims_matching_expression_version', options_list=['--claims-matching-expression-version'], help='[Preview] 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 23bf9e5a74b..c9d1460096e 100644 --- a/src/azure-cli/azure/cli/command_modules/identity/commands.py +++ b/src/azure-cli/azure/cli/command_modules/identity/commands.py @@ -39,9 +39,10 @@ def load_command_table(self, _): with self.command_group('identity federated-credential', federated_identity_credentials_sdk, client_factory=_msi_federated_identity_credentials_operations, - min_api='2025-01-31-PREVIEW') as g: + min_api='2025-01-31-PREVIEW', + is_preview=True) 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') g.custom_command('delete', 'delete_federated_credential', confirmation=True) - g.custom_command('list', 'list_federated_credential') \ No newline at end of file + g.custom_command('list', 'list_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 704d3c2ad53..6be2a6e7d1a 100644 --- a/src/azure-cli/azure/cli/command_modules/identity/custom.py +++ b/src/azure-cli/azure/cli/command_modules/identity/custom.py @@ -9,7 +9,6 @@ MutuallyExclusiveArgumentError ) - def list_user_assigned_identities(cmd, resource_group_name=None): from azure.cli.command_modules.identity._client_factory import _msi_client_factory client = _msi_client_factory(cmd.cli_ctx) @@ -17,32 +16,31 @@ def list_user_assigned_identities(cmd, resource_group_name=None): return client.user_assigned_identities.list_by_resource_group(resource_group_name) return client.user_assigned_identities.list_by_subscription() - def create_identity(client, resource_group_name, resource_name, location, tags=None): parameters = {} parameters['location'] = location if tags is not None: parameters['tags'] = tags return client.create_or_update(resource_group_name=resource_group_name, - resource_name=resource_name, - parameters=parameters) - + resource_name=resource_name, + parameters=parameters) def list_identity_resources(cmd, resource_group_name, resource_name): from azure.cli.command_modules.identity._client_factory import _msi_list_resources_client client = _msi_list_resources_client(cmd.cli_ctx) return client.list_associated_resources(resource_group_name=resource_group_name, - resource_name=resource_name) - + resource_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, claims_matching_expression_value=None, claims_matching_expression_version=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: - recommendation = "Please provide the issuer URL using --issuer parameter. For GitHub Actions, use 'https://token.actions.githubusercontent.com'" + 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 @@ -50,37 +48,43 @@ def create_or_update_federated_credential(cmd, client, resource_group_name, iden 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" + ("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) - + 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, - 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, - parameters=parameters) + 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, + 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, + parameters=parameters) def delete_federated_credential(client, resource_group_name, identity_name, federated_credential_name): - return client.delete(resource_group_name=resource_group_name, resource_name=identity_name, - federated_identity_credential_resource_name=federated_credential_name) - + return client.delete(resource_group_name=resource_group_name, + resource_name=identity_name, + federated_identity_credential_resource_name=federated_credential_name) def show_federated_credential(client, resource_group_name, identity_name, federated_credential_name): - return client.get(resource_group_name=resource_group_name, resource_name=identity_name, - federated_identity_credential_resource_name=federated_credential_name) - + return client.get(resource_group_name=resource_group_name, + resource_name=identity_name, + federated_identity_credential_resource_name=federated_credential_name) def list_federated_credential(client, resource_group_name, identity_name): - return client.list(resource_group_name=resource_group_name, resource_name=identity_name) \ No newline at end of file + return client.list(resource_group_name=resource_group_name, resource_name=identity_name) From c9a08dcf135442397f9bdb56c78ace3d94c8d17c Mon Sep 17 00:00:00 2001 From: Srujan Bandarkar Date: Wed, 30 Apr 2025 23:18:18 -0400 Subject: [PATCH 5/6] use multi package for FIC --- .../azure/cli/command_modules/identity/_client_factory.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/azure-cli/azure/cli/command_modules/identity/_client_factory.py b/src/azure-cli/azure/cli/command_modules/identity/_client_factory.py index a549775369d..ee79c7555f0 100644 --- a/src/azure-cli/azure/cli/command_modules/identity/_client_factory.py +++ b/src/azure-cli/azure/cli/command_modules/identity/_client_factory.py @@ -26,5 +26,9 @@ def _msi_operations_operations(cli_ctx, _): return _msi_client_factory(cli_ctx).operations -def _msi_federated_identity_credentials_operations(cli_ctx, _): - return _msi_client_factory(cli_ctx).federated_identity_credentials +def _msi_federated_identity_credentials_operations(cli_ctx, **_): + """ + api version is specified for federated identity credentials command because new api version (2023-01-31) of MSI does not support + flexible fic command. In order to avoid a breaking change, multi-api package is used. + """ + return _msi_client_factory(cli_ctx, api_version='2025-01-31-PREVIEW').federated_identity_credentials From e0745252a8011f9941bc3c938e946fa9f914fff3 Mon Sep 17 00:00:00 2001 From: Srujan Bandarkar Date: Wed, 30 Apr 2025 23:44:48 -0400 Subject: [PATCH 6/6] test --- .../cli/command_modules/identity/__init__.py | 22 ++++-- .../cli/command_modules/identity/_help.py | 78 ++++++++++--------- .../cli/command_modules/identity/_params.py | 59 ++++++++++---- .../cli/command_modules/identity/commands.py | 44 ++++------- 4 files changed, 116 insertions(+), 87 deletions(-) diff --git a/src/azure-cli/azure/cli/command_modules/identity/__init__.py b/src/azure-cli/azure/cli/command_modules/identity/__init__.py index 63f5d8f56a4..beac0422253 100644 --- a/src/azure-cli/azure/cli/command_modules/identity/__init__.py +++ b/src/azure-cli/azure/cli/command_modules/identity/__init__.py @@ -7,15 +7,28 @@ from azure.cli.core import AzCommandsLoader from azure.cli.core.profiles import ResourceType - class IdentityCommandsLoader(AzCommandsLoader): def __init__(self, cli_ctx=None): from azure.cli.core.commands import CliCommandType - identity_custom = CliCommandType(operations_tmpl='azure.cli.command_modules.identity.custom#{}') + from azure.cli.command_modules.identity._client_factory import (_msi_user_identities_operations, + _msi_federated_identity_credentials_operations) + + # Base identity commands + identity_custom = CliCommandType( + operations_tmpl='azure.cli.command_modules.identity.custom#{}', + client_factory=_msi_user_identities_operations + ) + + # Federated credential commands + federated_identity_custom = CliCommandType( + operations_tmpl='azure.cli.command_modules.identity.custom#{}', + client_factory=_msi_federated_identity_credentials_operations + ) + super().__init__(cli_ctx=cli_ctx, - resource_type=ResourceType.MGMT_MSI, - custom_command_type=identity_custom) + resource_type=ResourceType.MGMT_MSI, + custom_command_type=identity_custom) def load_command_table(self, args): from azure.cli.command_modules.identity.commands import load_command_table @@ -26,5 +39,4 @@ def load_arguments(self, command): from azure.cli.command_modules.identity._params import load_arguments load_arguments(self, command) - COMMAND_LOADER_CLS = IdentityCommandsLoader 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 6ed29dbd33f..d76cc4d25e7 100644 --- a/src/azure-cli/azure/cli/command_modules/identity/_help.py +++ b/src/azure-cli/azure/cli/command_modules/identity/_help.py @@ -3,8 +3,6 @@ # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- -# pylint: disable=line-too-long, too-many-lines - from knack.help_files import helps helps['identity'] = """ @@ -14,7 +12,7 @@ helps['identity create'] = """ type: command -short-summary: Create Identities. +short-summary: Create an identity. examples: - name: Create an identity. text: | @@ -36,61 +34,71 @@ short-summary: List the associated resources for the identity. """ +helps['identity show'] = """ +type: command +short-summary: Show the details of a managed identity. +""" + +helps['identity delete'] = """ +type: command +short-summary: Delete a managed identity. +""" + helps['identity federated-credential'] = """ type: group -short-summary: "[Preview] Manage federated identity credentials under user assigned identities." -min_api: 2025-01-31-PREVIEW +short-summary: [Preview] Manage federated credentials under managed identities. """ helps['identity federated-credential create'] = """ type: command -short-summary: "[Preview] Create a federated identity credential under an existing user assigned identity." -min_api: 2025-01-31-PREVIEW +short-summary: [Preview] Create a federated credential. +parameters: + - name: --name -n + type: string + short-summary: Name of the federated credential. + long-summary: Must start with a letter or number, and can contain letters, numbers, underscores, and hyphens. Length must be between 3-120 characters. + - name: --identity-name + type: string + short-summary: Name of the managed identity. + - name: --issuer + type: string + short-summary: The URL of the issuer to be trusted. + long-summary: For GitHub Actions, use 'https://token.actions.githubusercontent.com' + - name: --subject + type: string + short-summary: The identifier of the external identity. + long-summary: Cannot be used with claims-matching-expression-* parameters. + - name: --audiences + type: array + short-summary: List of audiences that can appear in the issued token. examples: - - 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. + - name: Create a federated identity credential with subject matching 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 + az identity federated-credential create -g MyResourceGroup --identity-name MyIdentity -n MyFicName \\ + --issuer https://token.actions.githubusercontent.com \\ + --subject "system:serviceaccount:ns:svcaccount" \\ + --audiences api://AzureADTokenExchange """ helps['identity federated-credential update'] = """ type: command -short-summary: "[Preview] Update a federated identity credential under an existing user assigned identity." -min_api: 2025-01-31-PREVIEW -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 +short-summary: [Preview] Update a federated credential. """ helps['identity federated-credential delete'] = """ type: command -short-summary: "[Preview] Delete a federated identity credential under an existing user assigned identity." -min_api: 2025-01-31-PREVIEW +short-summary: [Preview] Delete a federated credential. 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 + - name: Delete a federated credential + text: az identity federated-credential delete -g MyResourceGroup --identity-name MyIdentity -n MyFicName """ helps['identity federated-credential show'] = """ type: command -short-summary: "[Preview] Show a federated identity credential under an existing user assigned identity." -min_api: 2025-01-31-PREVIEW -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 +short-summary: [Preview] Show details of a federated credential. """ helps['identity federated-credential list'] = """ type: command -short-summary: "[Preview] List all federated identity credentials under an existing user assigned identity." -min_api: 2025-01-31-PREVIEW -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 +short-summary: [Preview] List all federated credentials for a managed identity. """ 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 6eb7f50e084..c5ed3bee478 100644 --- a/src/azure-cli/azure/cli/command_modules/identity/_params.py +++ b/src/azure-cli/azure/cli/command_modules/identity/_params.py @@ -3,18 +3,14 @@ # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- -# pylint: disable=line-too-long, too-many-lines from knack.arguments import CLIArgumentType - from azure.cli.core.commands.parameters import get_location_type, tags_type - name_arg_type = CLIArgumentType(options_list=('--name', '-n'), metavar='NAME', - help='The name of the identity resource.') - + help='The name of the identity resource.') def load_arguments(self, _): - + # Base identity parameters with self.argument_context('identity') as c: c.argument('resource_name', arg_type=name_arg_type, id_part='name') @@ -22,14 +18,43 @@ 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='2025-01-31-PREVIEW', is_preview=True) as c: - c.argument('federated_credential_name', options_list=('--name', '-n'), help='[Preview] The name of the federated identity credential resource.') - c.argument('identity_name', help='[Preview] The name of the identity resource.') - - for scope in ['identity federated-credential create', 'identity federated-credential update']: - with self.argument_context(scope, is_preview=True) as c: - c.argument('issuer', help='[Preview] 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='[Preview] 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='[Preview] 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='[Preview] The claims expression value for the federated identity credential.') - c.argument('claims_matching_expression_version', options_list=['--claims-matching-expression-version'], help='[Preview] The claims expression language version for the federated identity credential.') + # Register federated-credential parameters as part of the identity group + with self.argument_context('identity federated-credential', is_preview=True) as c: + c.argument('federated_credential_name', options_list=('--name', '-n'), + help='[Preview] The name of the federated identity credential resource. Must start with a letter, number and can contain letters, numbers, underscores, and hyphens. Length must be between 3-120 characters.', + type=str) + c.argument('identity_name', + help='[Preview] The name of the user assigned identity.') + + # Register create/update specific parameters + with self.argument_context('identity federated-credential create', is_preview=True) as c: + c.argument('issuer', + help='[Preview] The URL of the issuer to be trusted.', + required=True) + c.argument('subject', + help='[Preview] The identifier of the external identity. Cannot be used with claims-matching-expression-*.') + c.argument('audiences', + nargs='+', + help='[Preview] The list of audiences that can appear in the issued token.', + required=True) + c.argument('claims_matching_expression_value', + help='[Preview] The wildcard-based expression for matching incoming subject claims. Cannot be used with subject.') + c.argument('claims_matching_expression_version', + type=int, + help='[Preview] The version of the claims matching expression language.') + + with self.argument_context('identity federated-credential update', is_preview=True) as c: + c.argument('issuer', + help='[Preview] The URL of the issuer to be trusted.', + required=True) + c.argument('subject', + help='[Preview] The identifier of the external identity. Cannot be used with claims-matching-expression-*.') + c.argument('audiences', + nargs='+', + help='[Preview] The list of audiences that can appear in the issued token.', + required=True) + c.argument('claims_matching_expression_value', + help='[Preview] The wildcard-based expression for matching incoming subject claims. Cannot be used with subject.') + c.argument('claims_matching_expression_version', + type=int, + help='[Preview] The version of the claims matching expression language.') 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 c9d1460096e..a370294e127 100644 --- a/src/azure-cli/azure/cli/command_modules/identity/commands.py +++ b/src/azure-cli/azure/cli/command_modules/identity/commands.py @@ -3,46 +3,30 @@ # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- - from azure.cli.core.commands import CliCommandType -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 - ) - msi_operations_sdk = CliCommandType( - operations_tmpl='azure.mgmt.msi.operations#Operations.{}', - client_factory=_msi_operations_operations - ) - federated_identity_credentials_sdk = CliCommandType( - operations_tmpl='azure.mgmt.msi.operations#FederatedIdentityCredentialsOperations.{}', - client_factory=_msi_federated_identity_credentials_operations - ) - - with self.command_group('identity', identity_sdk, client_factory=_msi_user_identities_operations) as g: + # Base operations + with self.command_group('identity') as g: g.custom_command('create', 'create_identity', validator=process_msi_namespace) - g.show_command('show', 'get') - g.command('delete', 'delete') + g.command('show', 'get', operations_tmpl='azure.mgmt.msi.operations#UserAssignedIdentitiesOperations.{}') + g.command('delete', 'delete', operations_tmpl='azure.mgmt.msi.operations#UserAssignedIdentitiesOperations.{}') 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', operations_tmpl='azure.mgmt.msi.operations#Operations.{}') - 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='2025-01-31-PREVIEW', - is_preview=True) as g: + # Federated credential operations + with self.command_group('identity federated-credential', + operations_tmpl='azure.mgmt.msi.operations#FederatedIdentityCredentialsOperations.{}', + client_factory=_msi_federated_identity_credentials_operations, + is_preview=True) 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') g.custom_command('delete', 'delete_federated_credential', confirmation=True) + g.custom_command('show', 'show_federated_credential') g.custom_command('list', 'list_federated_credential')