From 527d9cc5ad1cf4af420bd7370cf3c6318a61f77a Mon Sep 17 00:00:00 2001 From: Srujan Bandarkar Date: Wed, 30 Apr 2025 12:19:35 -0400 Subject: [PATCH 01/10] [Managed_Service_Identity] Added support for FlexibleFIC --- .../azure/cli/command_modules/identity/_help.py | 6 ++++++ .../azure/cli/command_modules/identity/_params.py | 4 +++- .../azure/cli/command_modules/identity/commands.py | 2 +- .../azure/cli/command_modules/identity/custom.py | 11 +++++++++-- 4 files changed, 19 insertions(+), 4 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..84bb19f1681 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 expression + 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'] = """ @@ -57,6 +60,9 @@ - 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 + text: | + az identity federated-credential update --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 delete'] = """ 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..d9b15e4d423 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 that will be evaluated by Azure AD to issue a token. For example, claims[\'sub\'] startswith \'repo:contoso-org/contoso-repo:ref:refs/heads\'.') + c.argument('claims_matching_expression_version', options_list=['--claims-matching-expression-version'], help='The version of claims expression language. For example, 1.') 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..27b846b5ab2 100644 --- a/src/azure-cli/azure/cli/command_modules/identity/commands.py +++ b/src/azure-cli/azure/cli/command_modules/identity/commands.py @@ -39,7 +39,7 @@ 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') 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..55560cf69f9 100644 --- a/src/azure-cli/azure/cli/command_modules/identity/custom.py +++ b/src/azure-cli/azure/cli/command_modules/identity/custom.py @@ -35,7 +35,8 @@ 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: @@ -43,7 +44,13 @@ def create_or_update_federated_credential(cmd, client, resource_group_name, iden 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, From df6901bbdef55116b53935731910324ba590c361 Mon Sep 17 00:00:00 2001 From: Srujan Bandarkar Date: Wed, 30 Apr 2025 12:50:07 -0400 Subject: [PATCH 02/10] adding tests --- .../cli/command_modules/identity/_help.py | 8 ++-- .../identity/tests/latest/test_identity.py | 39 +++++++++++++------ 2 files changed, 31 insertions(+), 16 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 84bb19f1681..15978512025 100644 --- a/src/azure-cli/azure/cli/command_modules/identity/_help.py +++ b/src/azure-cli/azure/cli/command_modules/identity/_help.py @@ -48,9 +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 expression + - name: Create a federated identity credential with claims matching expression for GitHub 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 --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 update'] = """ @@ -60,9 +60,9 @@ - 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 + - 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://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 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'] = """ 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..08e77a9da02 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,23 @@ 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 + # test updating federated identity credential with claims matching expression + self.kwargs.update({ + 'new_claims_expr': "claims['sub'] startswith 'repo:contoso-org/updated-repo:ref:refs/heads'" + }) 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('claimsMatchingExpressionValue', '{new_claims_expr}'), + self.check('claimsMatchingExpressionVersion', '{claims_ver}') ]) # delete a federated identity credential @@ -106,8 +120,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 From 93e4cfebd002a01c48bc4ad3ee90fc204cf136de Mon Sep 17 00:00:00 2001 From: Srujan Bandarkar Date: Wed, 30 Apr 2025 12:59:34 -0400 Subject: [PATCH 03/10] properly register the federated-credential subcommands --- .../cli/command_modules/identity/commands.py | 37 ++++++++++--------- 1 file changed, 19 insertions(+), 18 deletions(-) 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 27b846b5ab2..3055c6f55b4 100644 --- a/src/azure-cli/azure/cli/command_modules/identity/commands.py +++ b/src/azure-cli/azure/cli/command_modules/identity/commands.py @@ -27,21 +27,22 @@ def load_command_table(self, _): client_factory=_msi_federated_identity_credentials_operations ) - with self.command_group('identity', identity_sdk, client_factory=_msi_user_identities_operations) 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') - - 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') 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') + with self.command_group('identity', is_preview=True) as g: + g.command_group('', identity_sdk, client_factory=_msi_user_identities_operations) as g2: + g2.custom_command('create', 'create_identity', validator=process_msi_namespace) + g2.show_command('show', 'get') + g2.command('delete', 'delete') + g2.custom_command('list', 'list_user_assigned_identities') + g2.custom_command('list-resources', 'list_identity_resources', min_api='2021-09-30-preview') + + g.command_group('', msi_operations_sdk, client_factory=_msi_operations_operations) as g2: + g2.command('list-operations', 'list') + + g.command_group('federated-credential', federated_identity_credentials_sdk, + client_factory=_msi_federated_identity_credentials_operations, + min_api='2025-01-31-PREVIEW') as g2: + g2.custom_command('create', 'create_or_update_federated_credential') + g2.custom_command('update', 'create_or_update_federated_credential') + g2.custom_show_command('show', 'show_federated_credential') + g2.custom_command('delete', 'delete_federated_credential', confirmation=True) + g2.custom_command('list', 'list_federated_credential') From 3ff4fbd70bbb3c5e6b02b24c7c81f13e6e27bae2 Mon Sep 17 00:00:00 2001 From: Srujan Bandarkar Date: Wed, 30 Apr 2025 13:09:06 -0400 Subject: [PATCH 04/10] update --- .../cli/command_modules/identity/_help.py | 125 ++++++++++-------- .../cli/command_modules/identity/commands.py | 37 +++--- 2 files changed, 91 insertions(+), 71 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 15978512025..c8a89d94a9d 100644 --- a/src/azure-cli/azure/cli/command_modules/identity/_help.py +++ b/src/azure-cli/azure/cli/command_modules/identity/_help.py @@ -8,86 +8,107 @@ 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'] = """ -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. - 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 expression for GitHub - text: | - az identity federated-credential create --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 + 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. + 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 expression for GitHub + text: | + az identity federated-credential create --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 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 - - 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 + 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/commands.py b/src/azure-cli/azure/cli/command_modules/identity/commands.py index 3055c6f55b4..fb56ce07e50 100644 --- a/src/azure-cli/azure/cli/command_modules/identity/commands.py +++ b/src/azure-cli/azure/cli/command_modules/identity/commands.py @@ -27,22 +27,21 @@ def load_command_table(self, _): client_factory=_msi_federated_identity_credentials_operations ) - with self.command_group('identity', is_preview=True) as g: - g.command_group('', identity_sdk, client_factory=_msi_user_identities_operations) as g2: - g2.custom_command('create', 'create_identity', validator=process_msi_namespace) - g2.show_command('show', 'get') - g2.command('delete', 'delete') - g2.custom_command('list', 'list_user_assigned_identities') - g2.custom_command('list-resources', 'list_identity_resources', min_api='2021-09-30-preview') - - g.command_group('', msi_operations_sdk, client_factory=_msi_operations_operations) as g2: - g2.command('list-operations', 'list') - - g.command_group('federated-credential', federated_identity_credentials_sdk, - client_factory=_msi_federated_identity_credentials_operations, - min_api='2025-01-31-PREVIEW') as g2: - g2.custom_command('create', 'create_or_update_federated_credential') - g2.custom_command('update', 'create_or_update_federated_credential') - g2.custom_show_command('show', 'show_federated_credential') - g2.custom_command('delete', 'delete_federated_credential', confirmation=True) - g2.custom_command('list', 'list_federated_credential') + with self.command_group('identity', identity_sdk, client_factory=_msi_user_identities_operations, 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') + + with self.command_group('identity', msi_operations_sdk, client_factory=_msi_operations_operations, is_preview=True) 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, + is_preview=True, 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') From d28b096a5211c7193871ec46f7a6b0ca9d00ec9d Mon Sep 17 00:00:00 2001 From: Srujan Bandarkar Date: Wed, 30 Apr 2025 13:25:51 -0400 Subject: [PATCH 05/10] update --- .../azure/cli/command_modules/identity/commands.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 fb56ce07e50..ea07a7b433d 100644 --- a/src/azure-cli/azure/cli/command_modules/identity/commands.py +++ b/src/azure-cli/azure/cli/command_modules/identity/commands.py @@ -27,19 +27,19 @@ def load_command_table(self, _): client_factory=_msi_federated_identity_credentials_operations ) - with self.command_group('identity', identity_sdk, client_factory=_msi_user_identities_operations, is_preview=True) as g: + with self.command_group('identity', identity_sdk, client_factory=_msi_user_identities_operations) 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') - with self.command_group('identity', msi_operations_sdk, client_factory=_msi_operations_operations, is_preview=True) as g: + 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, - is_preview=True, min_api='2025-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') From f62b4f46561a21bdd3005d6a619c3389a84cb3a0 Mon Sep 17 00:00:00 2001 From: Srujan Bandarkar Date: Wed, 30 Apr 2025 13:35:18 -0400 Subject: [PATCH 06/10] update --- src/azure-cli/azure/cli/command_modules/identity/commands.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 ea07a7b433d..dbb82734d26 100644 --- a/src/azure-cli/azure/cli/command_modules/identity/commands.py +++ b/src/azure-cli/azure/cli/command_modules/identity/commands.py @@ -39,7 +39,7 @@ 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') 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') From c4590279445ceb6a5eeb0f7b78883fafb2bce864 Mon Sep 17 00:00:00 2001 From: Srujan Bandarkar Date: Wed, 30 Apr 2025 13:42:35 -0400 Subject: [PATCH 07/10] fix --- src/azure-cli/azure/cli/command_modules/identity/commands.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 dbb82734d26..ea07a7b433d 100644 --- a/src/azure-cli/azure/cli/command_modules/identity/commands.py +++ b/src/azure-cli/azure/cli/command_modules/identity/commands.py @@ -39,7 +39,7 @@ 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') 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') From 5c10f98b6f5ffe5c4b1d5c591fd574c8bf3e6a29 Mon Sep 17 00:00:00 2001 From: Srujan Bandarkar Date: Wed, 30 Apr 2025 13:53:38 -0400 Subject: [PATCH 08/10] test --- .../azure/cli/command_modules/__init__.py | 10 + .../cli/command_modules/identity/__init__.py | 14 +- .../identity/azext_metadata.json | 6 + .../cli/command_modules/identity/commands.py | 35 ++-- src/azure-cli/setup.py | 190 +----------------- 5 files changed, 55 insertions(+), 200 deletions(-) create mode 100644 src/azure-cli/azure/cli/command_modules/identity/azext_metadata.json diff --git a/src/azure-cli/azure/cli/command_modules/__init__.py b/src/azure-cli/azure/cli/command_modules/__init__.py index e69de29bb2d..0fc98de8e38 100644 --- a/src/azure-cli/azure/cli/command_modules/__init__.py +++ b/src/azure-cli/azure/cli/command_modules/__init__.py @@ -0,0 +1,10 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +""" +Reserved for command module operations. +""" + +__path__ = __import__('pkgutil').extend_path(__path__, __name__) 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..f8642714e0c 100644 --- a/src/azure-cli/azure/cli/command_modules/identity/__init__.py +++ b/src/azure-cli/azure/cli/command_modules/identity/__init__.py @@ -4,18 +4,22 @@ # -------------------------------------------------------------------------------------------- import azure.cli.command_modules.identity._help # pylint: disable=unused-import + from azure.cli.core import AzCommandsLoader +from azure.cli.core.commands import CliCommandType 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#{}') + identity_custom = CliCommandType( + operations_tmpl='azure.cli.command_modules.identity.custom#{}', + client_factory=None + ) 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, + min_profile='2020-09-01-hybrid') def load_command_table(self, args): from azure.cli.command_modules.identity.commands import load_command_table diff --git a/src/azure-cli/azure/cli/command_modules/identity/azext_metadata.json b/src/azure-cli/azure/cli/command_modules/identity/azext_metadata.json new file mode 100644 index 00000000000..6cff71c95d0 --- /dev/null +++ b/src/azure-cli/azure/cli/command_modules/identity/azext_metadata.json @@ -0,0 +1,6 @@ +{ + "azext.isPreview": true, + "azext.minCliCoreVersion": "2.72.0", + "azext.maxCliCoreVersion": "3.0.0", + "version": "0.1.0" +} 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 ea07a7b433d..5ebd4beb634 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,46 @@ # 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='2025-01-31-PREVIEW') as g: + # Federated identity credential commands as a subgroup + with self.command_group('identity federated-credential', + federated_identity_credentials_sdk, + is_preview=True, + 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/setup.py b/src/azure-cli/setup.py index e427a5efca3..f8193063f6c 100644 --- a/src/azure-cli/setup.py +++ b/src/azure-cli/setup.py @@ -1,209 +1,41 @@ #!/usr/bin/env python -# -------------------------------------------------------------------------------------------- -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. See License.txt in the project root for license information. -# -------------------------------------------------------------------------------------------- +from setuptools import setup, find_namespace_packages -from codecs import open -from setuptools import setup, find_packages -import sys - -try: - from azure_cli_bdist_wheel import cmdclass -except ImportError: - import logging - - logging.warning("Wheel is not available, disabling bdist_wheel hook") - cmdclass = {} - -VERSION = "2.72.0" -# If we have source, validate that our version numbers match -# This should prevent uploading releases with mismatched versions. -try: - with open('azure/cli/__main__.py', 'r', encoding='utf-8') as f: - content = f.read() -except OSError: - pass -else: - import re - - m = re.search(r'__version__\s*=\s*[\'"](.+?)[\'"]', content) - if not m: - print('Could not find __version__ in azure/cli/__main__.py') - sys.exit(1) - if m.group(1) != VERSION: - print('Expected __version__ = "{}"; found "{}"'.format(VERSION, m.group(1))) - sys.exit(1) +VERSION = "1.0.0" CLASSIFIERS = [ 'Development Status :: 5 - Production/Stable', 'Intended Audience :: Developers', 'Intended Audience :: System Administrators', 'Programming Language :: Python', - 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', - 'Programming Language :: Python :: 3.12', 'License :: OSI Approved :: MIT License', ] DEPENDENCIES = [ - "antlr4-python3-runtime~=4.13.1", - 'azure-appconfiguration~=1.7.0', - 'azure-batch~=15.0.0b1', 'azure-cli-core=={}'.format(VERSION), - 'azure-cosmos~=3.0,>=3.0.2', - 'azure-data-tables==12.4.0', - 'azure-datalake-store~=1.0.0a0', - 'azure-keyvault-administration==4.4.0b2', - 'azure-keyvault-certificates==4.7.0', - 'azure-keyvault-keys==4.9.0b3', - 'azure-keyvault-secrets==4.7.0', - 'azure-mgmt-advisor==9.0.0', - 'azure-mgmt-apimanagement==4.0.0', - 'azure-mgmt-appconfiguration==3.1.0', - 'azure-mgmt-appcontainers==2.0.0', - 'azure-mgmt-applicationinsights~=1.0.0', - 'azure-mgmt-authorization~=4.0.0', - 'azure-mgmt-batchai==7.0.0b1', - 'azure-mgmt-batch~=17.3.0', - 'azure-mgmt-billing==6.0.0', - 'azure-mgmt-botservice~=2.0.0b3', - 'azure-mgmt-cdn==12.0.0', - 'azure-mgmt-cognitiveservices~=13.5.0', - 'azure-mgmt-compute~=34.1.0', - 'azure-mgmt-containerinstance==10.2.0b1', - 'azure-mgmt-containerregistry==13.0.0', - 'azure-mgmt-containerservice~=35.0.0', - 'azure-mgmt-cosmosdb==9.7.0', - 'azure-mgmt-databoxedge~=1.0.0', - 'azure-mgmt-datalake-store~=1.1.0b1', - 'azure-mgmt-datamigration~=10.0.0', - 'azure-mgmt-dns~=8.0.0', - 'azure-mgmt-eventgrid==10.2.0b2', - 'azure-mgmt-eventhub~=10.1.0', - 'azure-mgmt-extendedlocation==1.0.0b2', - 'azure-mgmt-hdinsight==9.0.0b3', - 'azure-mgmt-imagebuilder~=1.3.0', - 'azure-mgmt-iotcentral~=10.0.0b1', - 'azure-mgmt-iothub==3.0.0', - 'azure-mgmt-iothubprovisioningservices==1.1.0', - 'azure-mgmt-keyvault==11.0.0', - 'azure-mgmt-loganalytics==13.0.0b4', - 'azure-mgmt-managementgroups~=1.0.0', - 'azure-mgmt-maps~=2.0.0', - 'azure-mgmt-marketplaceordering==1.1.0', - 'azure-mgmt-media~=9.0', - 'azure-mgmt-monitor~=5.0.0', 'azure-mgmt-msi~=7.0.0', - 'azure-mgmt-netapp~=10.1.0', - 'azure-mgmt-policyinsights==1.1.0b4', - 'azure-mgmt-postgresqlflexibleservers==1.1.0b2', - 'azure-mgmt-privatedns~=1.0.0', - 'azure-mgmt-rdbms==10.2.0b17', - 'azure-mgmt-mysqlflexibleservers==1.0.0b3', - 'azure-mgmt-recoveryservicesbackup~=9.1.0', - 'azure-mgmt-recoveryservices~=3.0.0', - 'azure-mgmt-redhatopenshift~=1.5.0', - 'azure-mgmt-redis~=14.5.0', - 'azure-mgmt-resource==23.1.1', - 'azure-mgmt-search~=9.0', - 'azure-mgmt-security==6.0.0', - 'azure-mgmt-servicebus~=8.2.0', - 'azure-mgmt-servicefabricmanagedclusters==2.1.0b1', - 'azure-mgmt-servicelinker==1.2.0b3', - 'azure-mgmt-servicefabric~=2.1.0', - 'azure-mgmt-signalr==2.0.0b2', - 'azure-mgmt-sqlvirtualmachine==1.0.0b5', - 'azure-mgmt-sql==4.0.0b21', - 'azure-mgmt-storage==22.1.0', - 'azure-mgmt-synapse==2.1.0b5', - 'azure-mgmt-trafficmanager~=1.0.0', - 'azure-mgmt-web==7.3.1', - 'azure-monitor-query==1.2.0', - 'azure-multiapi-storage==1.4.0', - 'azure-storage-common~=1.4', - 'azure-synapse-accesscontrol~=0.5.0', - 'azure-synapse-artifacts~=0.20.0', - 'azure-synapse-managedprivateendpoints~=0.4.0', - 'azure-synapse-spark~=0.7.0', - 'chardet~=5.2.0', - 'colorama~=0.4.4', - # On Linux, the distribution (Ubuntu, Debian, etc) and version are checked for `az feedback` - 'distro; sys_platform == "linux"', - 'fabric~=3.2.2', - 'javaproperties~=0.5.1', - 'jsondiff~=2.0.0', - 'packaging>=20.9', - 'paramiko>=2.0.8,<4.0.0', - 'pycomposefile>=0.0.32', - 'PyGithub~=1.38', - 'PyNaCl~=1.5.0', - 'scp~=0.13.2', - 'semver==2.13.0', - 'setuptools', - 'six>=1.10.0', # six is still used by countless extensions - 'sshtunnel~=0.1.4', - # Even though knack already depends on tabulate, profile module directly uses it for interactive subscription - # selection - 'tabulate', - 'urllib3', - 'websocket-client~=1.3.1', - 'xmltodict~=0.12' ] -with open('README.rst', 'r', encoding='utf-8') as f: - README = f.read() - setup( - name='azure-cli', + name='azure-cli-identity', version=VERSION, - description='Microsoft Azure Command-Line Tools', - long_description=README, + description='Microsoft Azure Command-Line Tools Identity Command Module', + long_description='', license='MIT', author='Microsoft Corporation', author_email='azpycli@microsoft.com', url='https://github.com/Azure/azure-cli', - zip_safe=False, classifiers=CLASSIFIERS, - scripts=[ - 'az', - 'az.completion.sh', - 'az.bat', - 'azps.ps1' + namespace_packages=[ + 'azure', + 'azure.cli', + 'azure.cli.command_modules', ], - packages=find_packages(exclude=["*.tests", "*.tests.*", "tests.*", "tests", "azure", "azure.cli"]), + packages=find_namespace_packages(include=['azure.cli.command_modules.*']), install_requires=DEPENDENCIES, - python_requires='>=3.9.0', - package_data={ - 'azure.cli.command_modules.acr': ['*.json'], - 'azure.cli.command_modules.botservice': ['*.json', '*.config'], - 'azure.cli.command_modules.monitor.operations': ['autoscale-parameters-template.json'], - 'azure.cli.command_modules.servicefabric': [ - 'template/windows/template.json', - 'template/windows/parameter.json', - 'template/linux/template.json', - 'template/linux/parameter.json', - 'template/service/template.json', - 'template/service/parameter.json' - ], - 'azure.cli.command_modules.appservice': [ - 'resources/WebappRuntimeStacks.json', - 'resources/GenerateRandomAppNames.json' - ], - 'azure.cli.command_modules.rdbms': [ - '*.json', - 'randomname/adjectives.txt', - 'randomname/nouns.txt', - 'templates/mysql_githubaction_template.yaml', - 'templates/postgresql_githubaction_template.yaml' - ], - 'azure.cli.command_modules.mysql': [ - 'random/adjectives.txt', - 'random/nouns.txt' - ] - }, - cmdclass=cmdclass + python_requires='>=3.9.0' ) From 8c897c0044347e5eab72e45dec44fe677b85025f Mon Sep 17 00:00:00 2001 From: Srujan Bandarkar Date: Wed, 30 Apr 2025 14:31:49 -0400 Subject: [PATCH 09/10] [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 | 3 + .../cli/command_modules/identity/_params.py | 4 +- .../cli/command_modules/identity/commands.py | 2 +- .../cli/command_modules/identity/custom.py | 32 +++++++-- .../identity/tests/latest/test_identity.py | 69 ++++++++++++++++++- 5 files changed, 101 insertions(+), 9 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..ecf87e73094 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'] = """ 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..27b846b5ab2 100644 --- a/src/azure-cli/azure/cli/command_modules/identity/commands.py +++ b/src/azure-cli/azure/cli/command_modules/identity/commands.py @@ -39,7 +39,7 @@ 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') 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..afa81b267cd 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, 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..9aa364e02d9 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') From 3f78b9185b093f67692f4b22278c155b781b3ba8 Mon Sep 17 00:00:00 2001 From: Srujan Bandarkar Date: Wed, 30 Apr 2025 15:11:23 -0400 Subject: [PATCH 10/10] [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 | 3 + .../cli/command_modules/identity/_params.py | 4 +- .../cli/command_modules/identity/commands.py | 2 +- .../cli/command_modules/identity/custom.py | 34 +++++++-- .../identity/tests/latest/test_identity.py | 69 ++++++++++++++++++- 5 files changed, 102 insertions(+), 10 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..ecf87e73094 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'] = """ 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..27b846b5ab2 100644 --- a/src/azure-cli/azure/cli/command_modules/identity/commands.py +++ b/src/azure-cli/azure/cli/command_modules/identity/commands.py @@ -39,7 +39,7 @@ 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') 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