Skip to content

[App Service] Fix #30357: az webapp config ssl bind: Use full Site object to avoid Azure Policy denial#33061

Closed
seligj95 wants to merge 2 commits intoAzure:devfrom
seligj95:fix/30357-ssl-bind-partial-site
Closed

[App Service] Fix #30357: az webapp config ssl bind: Use full Site object to avoid Azure Policy denial#33061
seligj95 wants to merge 2 commits intoAzure:devfrom
seligj95:fix/30357-ssl-bind-partial-site

Conversation

@seligj95
Copy link
Contributor

Description

Fixes #30357

az webapp config ssl bind was constructing a minimal Site object with only host_name_ssl_states, location, and tags when calling begin_create_or_update. This caused Azure Policy (e.g. "HTTPS Only must be enabled") to deny the operation because policy-sensitive fields like httpsOnly were absent from the PUT payload.

Changes

  • custom.py: Modified _update_host_name_ssl_state to reuse the full existing Site object (already fetched by the caller) instead of constructing a minimal one. Only the host_name_ssl_states field is updated on the existing object before passing it to begin_create_or_update.
  • test_webapp_commands_thru_mock.py: Added TestUpdateHostNameSslState with two tests:
    • Verifies that the full Site object (including https_only, location, tags) is passed through
    • Verifies that the slot parameter is correctly forwarded

Testing

  • All 31 unit tests in test_webapp_commands_thru_mock.py pass
  • No pylint regressions

…Site object to avoid Azure Policy denial

Previously, _update_host_name_ssl_state constructed a minimal Site object with
only host_name_ssl_states, location, and tags. When passed to
begin_create_or_update, this caused Azure Policy (e.g. 'HTTPS Only must be
enabled') to deny the operation because policy-sensitive fields like httpsOnly
were missing from the payload.

The fix reuses the full existing Site object fetched from Azure, updating only
the host_name_ssl_states field. This preserves all policy-sensitive properties
(httpsOnly, ftpsState, etc.) in the PUT request.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings March 26, 2026 13:54
@azure-client-tools-bot-prd
Copy link

azure-client-tools-bot-prd bot commented Mar 26, 2026

❌AzureCLI-FullTest
️✔️acr
️✔️latest
️✔️3.12
️✔️3.13
️✔️acs
️✔️latest
️✔️3.12
️✔️3.13
️✔️advisor
️✔️latest
️✔️3.12
️✔️3.13
️✔️ams
️✔️latest
️✔️3.12
️✔️3.13
️✔️apim
️✔️latest
️✔️3.12
️✔️3.13
️✔️appconfig
️✔️latest
️✔️3.12
️✔️3.13
❌appservice
❌latest
❌3.12
Type Test Case Error Message Line
Failed test_webapp_ssl self = <azure.cli.testsdk.base.ExecutionResult object at 0x7fdf54d0eba0>
cli_ctx = <azure.cli.core.mock.DummyCli object at 0x7fdf57ef0d70>
command = 'webapp show -g clitest.rg000001 -n web-ssl-test000003 -s slot-ssl-test000004'
expect_failure = False

    def in_process_execute(self, cli_ctx, command, expect_failure=False):
        from io import StringIO
        from vcr.errors import CannotOverwriteExistingCassetteException
    
        if command.startswith('az '):
            command = command[3:]
    
        stdout_buf = StringIO()
        logging_buf = StringIO()
        try:
            # issue: stderr cannot be redirect in this form, as a result some failure information
            # is lost when command fails.
>           self.exit_code = cli_ctx.invoke(shlex.split(command), out_file=stdout_buf) or 0
                             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

src/azure-cli-testsdk/azure/cli/testsdk/base.py:303: 
                                        
env/lib/python3.12/site-packages/knack/cli.py:245: in invoke
    exit_code = self.exception_handler(ex)
                ^^^^^^^^^^^^^^^^^^^^^^^^^^
src/azure-cli-core/azure/cli/core/init.py:159: in exception_handler
    return handle_exception(ex)
           ^^^^^^^^^^^^^^^^^^^^
src/azure-cli-testsdk/azure/cli/testsdk/patches.py:33: in handle_main_exception
    raise ex
env/lib/python3.12/site-packages/knack/cli.py:233: in invoke
    cmd_result = self.invocation.execute(args)
                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
src/azure-cli-core/azure/cli/core/commands/init.py:678: in execute
    raise ex
src/azure-cli-core/azure/cli/core/commands/init.py:821: in run_jobs_serially
    results.append(self.run_job(expanded_arg, cmd_copy))
                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
src/azure-cli-core/azure/cli/core/commands/init.py:790: in run_job
    result = cmd_copy(params)
             ^^^^^^^^^^^^^^^^
src/azure-cli-core/azure/cli/core/commands/init.py:336: in call
    return self.handler(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
src/azure-cli-core/azure/cli/core/commands/command_operation.py:362: in handler
    show_exception_handler(ex)
src/azure-cli-core/azure/cli/core/commands/arm.py:476: in show_exception_handler
    raise ex
src/azure-cli-core/azure/cli/core/commands/command_operation.py:360: in handler
    return op(**command_args)
           ^^^^^^^^^^^^^^^^^^
src/azure-cli/azure/cli/command_modules/appservice/custom.py:2338: in show_app
    app = generic_site_operation(cmd.cli_ctx, resource_group_name, name, 'get', slot)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
src/azure-cli/azure/cli/command_modules/appservice/appservice_utils.py:23: in generic_site_operation
    return (operation(resource_group_name, name, slot)
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
env/lib/python3.12/site-packages/azure/core/tracing/decorator.py:119: in wrapper_use_tracer
    return func(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^
env/lib/python3.12/site-packages/azure/mgmt/web/v2024_11_01/operations/web_apps_operations.py:33277: in get_slot
    pipeline_response: PipelineResponse = self.client.pipeline.run(  # pylint: disable=protected-access
env/lib/python3.12/site-packages/azure/core/pipeline/base.py:242: in run
    return first_node.send(pipeline_request)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
env/lib/python3.12/site-packages/azure/core/pipeline/base.py:98: in send
    response = self.next.send(request)
               ^^^^^^^^^^^^^^^^^^^^^^^
env/lib/python3.12/site-packages/azure/core/pipeline/base.py:98: in send
    response = self.next.send(request)
               ^^^^^^^^^^^^^^^^^^^^^^^
env/lib/python3.12/site-packages/azure/core/pipeline/base.py:98: in send
    response = self.next.send(request)
               ^^^^^^^^^^^^^^^^^^^^^^^
env/lib/python3.12/site-packages/azure/core/pipeline/base.py:98: in send
    response = self.next.send(request)
               ^^^^^^^^^^^^^^^^^^^^^^^
env/lib/python3.12/site-packages/azure/core/pipeline/base.py:98: in send
    response = self.next.send(request)
               ^^^^^^^^^^^^^^^^^^^^^^^
env/lib/python3.12/site-packages/azure/mgmt/core/policies/base.py:95: in send
    response = self.next.send(request)
               ^^^^^^^^^^^^^^^^^^^^^^^
env/lib/python3.12/site-packages/azure/core/pipeline/policies/redirect.py:205: in send
    response = self.next.send(request)
               ^^^^^^^^^^^^^^^^^^^^^^^
env/lib/python3.12/site-packages/azure/core/pipeline/policies/retry.py:545: in send
    response = self.next.send(request)
               ^^^^^^^^^^^^^^^^^^^^^^^
env/lib/python3.12/site-packages/azure/core/pipeline/policies/authentication.py:194: in send
    response = self.next.send(request)
               ^^^^^^^^^^^^^^^^^^^^^^^
env/lib/python3.12/site-packages/azure/core/pipeline/base.py:98: in send
    response = self.next.send(request)
               ^^^^^^^^^^^^^^^^^^^^^^^
env/lib/python3.12/site-packages/azure/core/pipeline/base.py:98: in send
    response = self.next.send(request)
               ^^^^^^^^^^^^^^^^^^^^^^^
env/lib/python3.12/site-packages/azure/core/pipeline/base.py:98: in send
    response = self.next.send(request)
               ^^^^^^^^^^^^^^^^^^^^^^^
env/lib/python3.12/site-packages/azure/core/pipeline/base.py:98: in send
    response = self.next.send(request)
               ^^^^^^^^^^^^^^^^^^^^^^^
env/lib/python3.12/site-packages/azure/core/pipeline/base.py:98: in send
    response = self.next.send(request)
               ^^^^^^^^^^^^^^^^^^^^^^^
env/lib/python3.12/site-packages/azure/core/pipeline/base.py:130: in send
    self.sender.send(request.http_request, **request.context.options),
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
env/lib/python3.12/site-packages/azure/core/pipeline/transport/requests_basic.py:375: in send
    response = self.session.request(  # type: ignore
env/lib/python3.12/site-packages/requests/sessions.py:589: in request
    resp = self.send(prep, **send_kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
env/lib/python3.12/site-packages/requests/sessions.py:703: in send
    r = adapter.send(request, **kwargs)
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
env/lib/python3.12/site-packages/requests/adapters.py:667: in send
    resp = conn.urlopen(
env/lib/python3.12/site-packages/urllib3/connectionpool.py:787: in urlopen
    response = self.make_request(
env/lib/python3.12/site-packages/urllib3/connectionpool.py:534: in make_request
    response = conn.getresponse()
               ^^^^^^^^^^^^^^^^^^
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
          

self = <VCRRequestsHTTPSConnection/mnt/vss/work/1/s/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/recordings/test_webapp_ssl.yaml(host='management.azure.com', port=443) at 0x7fdf54b392e0>
 = False, kwargs = {}

    def getresponse(self, 
=False, **kwargs):
        """Retrieve the response"""
        # Check to see if the cassette has a response for this request. If so,
        # then return it
        if self.cassette.can_play_response_for(self._vcr_request):
            log.info(f"Playing response for {self._vcr_request} from cassette")
            response = self.cassette.play_response(self._vcr_request)
            return VCRHTTPResponse(response)
        else:
            if self.cassette.write_protected and self.cassette.filter_request(self._vcr_request):
>               raise CannotOverwriteExistingCassetteException(
                    cassette=self.cassette,
                    failed_request=self._vcr_request,
                )
E               vcr.errors.CannotOverwriteExistingCassetteException: Can't overwrite existing cassette ('/mnt/vss/_work/1/s/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/recordings/test_webapp_ssl.yaml') in your current record mode ('once').
E               No match for the request (<Request (GET) https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.Web/sites/web-ssl-test000003/slots/slot-ssl-test000004?api-version=2024-11-01>)&nbsp;was&nbsp;found.
E               Found 7 similar requests with 0 different matcher(s) :
E               
E               1 - (<Request (GET) https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.Web/sites/web-ssl-test000003/slots/slot-ssl-test000004?api-version=2024-11-01>).
E               Matchers succeeded : ['method', 'scheme', 'host', 'port', 'path', '_custom_request_query_matcher']
E               Matchers failed :
E               
E               2 - (<Request (GET) https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.Web/sites/web-ssl-test000003/slots/slot-ssl-test000004?api-version=2024-11-01>).
E               Matchers succeeded : ['method', 'scheme', 'host', 'port', 'path', '_custom_request_query_matcher']
E               Matchers failed :
E               
E               3 - (<Request (GET) https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.Web/sites/web-ssl-test000003/slots/slot-ssl-test000004?api-version=2024-11-01>).
E               Matchers succeeded : ['method', 'scheme', 'host', 'port', 'path', '_custom_request_query_matcher']
E               Matchers failed :
E               
E               4 - (<Request (GET) https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.Web/sites/web-ssl-test000003/slots/slot-ssl-test000004?api-version=2024-11-01>).
E               Matchers succeeded : ['method', 'scheme', 'host', 'port', 'path', '_custom_request_query_matcher']
E               Matchers failed :
E               
E               5 - (<Request (GET) https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.Web/sites/web-ssl-test000003/slots/slot-ssl-test000004?api-version=2024-11-01>).
E               Matchers succeeded : ['method', 'scheme', 'host', 'port', 'path', '_custom_request_query_matcher']
E               Matchers failed :
E               
E               6 - (<Request (GET) https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.Web/sites/web-ssl-test000003/slots/slot-ssl-test000004?api-version=2024-11-01>).
E               Matchers succeeded : ['method', 'scheme', 'host', 'port', 'path', '_custom_request_query_matcher']
E               Matchers failed :
E               
E               7 - (<Request (GET) https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.Web/sites/web-ssl-test000003/slots/slot-ssl-test000004?api-version=2024-11-01>).
E               Matchers succeeded : ['method', 'scheme', 'host', 'port', 'path', '_custom_request_query_matcher']
E               Matchers failed :

env/lib/python3.12/site-packages/vcr/stubs/init.py:277: CannotOverwriteExistingCassetteException

During handling of the above exception, another exception occurred:

self = <azure.cli.command_modules.appservice.tests.latest.test_webapp_commands.WebappSSLCertTest testMethod=test_webapp_ssl>
resource_group = 'clitest.rg000001', resource_group_location = 'westeurope'

    @AllowLargeResponse()
    @ResourceGroupPreparer(location=WINDOWS_ASP_LOCATION_WEBAPP)
    def test_webapp_ssl(self, resource_group, resource_group_location):
        plan = self.create_random_name(prefix='ssl-test-plan', length=24)
        webapp_name = self.create_random_name(prefix='web-ssl-test', length=20)
        slot_name = self.create_random_name(prefix='slot-ssl-test', length=20)
        # Cert Generated using
        # https://learn.microsoft.com/azure/app-service-web/web-sites-configure-ssl-certificate#bkmk_ssopenssl
        pfx_file = os.path.join(TEST_DIR, 'server.pfx')
        cert_password = 'test'
        cert_thumbprint = '9E9735C45C792B03B3FFCCA614852B32EE71AD6B'
        # we configure tags here in a hope to capture a repro for #6929
        self.cmd(
            'appservice plan create -g {} -n {} --sku S1 --tags plan=plan1'.format(resource_group, plan))
        self.cmd('appservice plan show -g {} -n {}'.format(resource_group,
                                                           plan), self.check('tags.plan', 'plan1'))
        self.cmd('webapp create -g {} -n {} --plan {} --tags web=web1'.format(
            resource_group, webapp_name, plan))
        self.cmd('webapp config ssl upload -g {} -n {} --certificate-file "{}" --certificate-password {} --certificate-name {}'.format(resource_group, webapp_name, pfx_file, cert_password, "test123"), checks=[
            JMESPathCheck('thumbprint', cert_thumbprint),
            JMESPathCheck('name', 'test123')
        ])
        self.cmd('webapp show -g {} -n {}'.format(resource_group,
                                                  webapp_name), self.check('tags.web', 'web1'))
        self.cmd('webapp config ssl bind -g {} -n {} --certificate-thumbprint {} --ssl-type {}'.format(resource_group, webapp_name, cert_thumbprint, 'SNI'), checks=[
            JMESPathCheck("hostNameSslStates
[?name=='{}.azurewebsites.net']
❌3.13
Type Test Case Error Message Line
Failed test_webapp_ssl self = <azure.cli.testsdk.base.ExecutionResult object at 0x7fb4cf8fc520>
cli_ctx = <azure.cli.core.mock.DummyCli object at 0x7fb4d04d9810>
command = 'webapp show -g clitest.rg000001 -n web-ssl-test000003 -s slot-ssl-test000004'
expect_failure = False

    def in_process_execute(self, cli_ctx, command, expect_failure=False):
        from io import StringIO
        from vcr.errors import CannotOverwriteExistingCassetteException
    
        if command.startswith('az '):
            command = command[3:]
    
        stdout_buf = StringIO()
        logging_buf = StringIO()
        try:
            # issue: stderr cannot be redirect in this form, as a result some failure information
            # is lost when command fails.
>           self.exit_code = cli_ctx.invoke(shlex.split(command), out_file=stdout_buf) or 0
                             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

src/azure-cli-testsdk/azure/cli/testsdk/base.py:303: 
                                        
env/lib/python3.13/site-packages/knack/cli.py:245: in invoke
    exit_code = self.exception_handler(ex)
                ^^^^^^^^^^^^^^^^^^^^^^^^^^
src/azure-cli-core/azure/cli/core/init.py:159: in exception_handler
    return handle_exception(ex)
           ^^^^^^^^^^^^^^^^^^^^
src/azure-cli-testsdk/azure/cli/testsdk/patches.py:33: in handle_main_exception
    raise ex
env/lib/python3.13/site-packages/knack/cli.py:233: in invoke
    cmd_result = self.invocation.execute(args)
                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
src/azure-cli-core/azure/cli/core/commands/init.py:678: in execute
    raise ex
src/azure-cli-core/azure/cli/core/commands/init.py:821: in run_jobs_serially
    results.append(self.run_job(expanded_arg, cmd_copy))
                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
src/azure-cli-core/azure/cli/core/commands/init.py:790: in run_job
    result = cmd_copy(params)
             ^^^^^^^^^^^^^^^^
src/azure-cli-core/azure/cli/core/commands/init.py:336: in call
    return self.handler(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
src/azure-cli-core/azure/cli/core/commands/command_operation.py:362: in handler
    show_exception_handler(ex)
src/azure-cli-core/azure/cli/core/commands/arm.py:476: in show_exception_handler
    raise ex
src/azure-cli-core/azure/cli/core/commands/command_operation.py:360: in handler
    return op(**command_args)
           ^^^^^^^^^^^^^^^^^^
src/azure-cli/azure/cli/command_modules/appservice/custom.py:2338: in show_app
    app = generic_site_operation(cmd.cli_ctx, resource_group_name, name, 'get', slot)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
src/azure-cli/azure/cli/command_modules/appservice/appservice_utils.py:23: in generic_site_operation
    return (operation(resource_group_name, name, slot)
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
env/lib/python3.13/site-packages/azure/core/tracing/decorator.py:119: in wrapper_use_tracer
    return func(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^
env/lib/python3.13/site-packages/azure/mgmt/web/v2024_11_01/operations/web_apps_operations.py:33277: in get_slot
    pipeline_response: PipelineResponse = self.client.pipeline.run(  # pylint: disable=protected-access
env/lib/python3.13/site-packages/azure/core/pipeline/base.py:242: in run
    return first_node.send(pipeline_request)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
env/lib/python3.13/site-packages/azure/core/pipeline/base.py:98: in send
    response = self.next.send(request)
               ^^^^^^^^^^^^^^^^^^^^^^^
env/lib/python3.13/site-packages/azure/core/pipeline/base.py:98: in send
    response = self.next.send(request)
               ^^^^^^^^^^^^^^^^^^^^^^^
env/lib/python3.13/site-packages/azure/core/pipeline/base.py:98: in send
    response = self.next.send(request)
               ^^^^^^^^^^^^^^^^^^^^^^^
env/lib/python3.13/site-packages/azure/core/pipeline/base.py:98: in send
    response = self.next.send(request)
               ^^^^^^^^^^^^^^^^^^^^^^^
env/lib/python3.13/site-packages/azure/core/pipeline/base.py:98: in send
    response = self.next.send(request)
               ^^^^^^^^^^^^^^^^^^^^^^^
env/lib/python3.13/site-packages/azure/mgmt/core/policies/base.py:95: in send
    response = self.next.send(request)
               ^^^^^^^^^^^^^^^^^^^^^^^
env/lib/python3.13/site-packages/azure/core/pipeline/policies/redirect.py:205: in send
    response = self.next.send(request)
               ^^^^^^^^^^^^^^^^^^^^^^^
env/lib/python3.13/site-packages/azure/core/pipeline/policies/retry.py:545: in send
    response = self.next.send(request)
               ^^^^^^^^^^^^^^^^^^^^^^^
env/lib/python3.13/site-packages/azure/core/pipeline/policies/authentication.py:194: in send
    response = self.next.send(request)
               ^^^^^^^^^^^^^^^^^^^^^^^
env/lib/python3.13/site-packages/azure/core/pipeline/base.py:98: in send
    response = self.next.send(request)
               ^^^^^^^^^^^^^^^^^^^^^^^
env/lib/python3.13/site-packages/azure/core/pipeline/base.py:98: in send
    response = self.next.send(request)
               ^^^^^^^^^^^^^^^^^^^^^^^
env/lib/python3.13/site-packages/azure/core/pipeline/base.py:98: in send
    response = self.next.send(request)
               ^^^^^^^^^^^^^^^^^^^^^^^
env/lib/python3.13/site-packages/azure/core/pipeline/base.py:98: in send
    response = self.next.send(request)
               ^^^^^^^^^^^^^^^^^^^^^^^
env/lib/python3.13/site-packages/azure/core/pipeline/base.py:98: in send
    response = self.next.send(request)
               ^^^^^^^^^^^^^^^^^^^^^^^
env/lib/python3.13/site-packages/azure/core/pipeline/base.py:130: in send
    self.sender.send(request.http_request, **request.context.options),
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
env/lib/python3.13/site-packages/azure/core/pipeline/transport/requests_basic.py:375: in send
    response = self.session.request(  # type: ignore
env/lib/python3.13/site-packages/requests/sessions.py:589: in request
    resp = self.send(prep, **send_kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
env/lib/python3.13/site-packages/requests/sessions.py:703: in send
    r = adapter.send(request, **kwargs)
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
env/lib/python3.13/site-packages/requests/adapters.py:667: in send
    resp = conn.urlopen(
env/lib/python3.13/site-packages/urllib3/connectionpool.py:787: in urlopen
    response = self.make_request(
env/lib/python3.13/site-packages/urllib3/connectionpool.py:534: in make_request
    response = conn.getresponse()
               ^^^^^^^^^^^^^^^^^^
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
          

self = <VCRRequestsHTTPSConnection/mnt/vss/work/1/s/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/recordings/test_webapp_ssl.yaml(host='management.azure.com', port=443) at 0x7fb4ccb6e190>
 = False, kwargs = {}

    def getresponse(self, 
=False, **kwargs):
        """Retrieve the response"""
        # Check to see if the cassette has a response for this request. If so,
        # then return it
        if self.cassette.can_play_response_for(self._vcr_request):
            log.info(f"Playing response for {self._vcr_request} from cassette")
            response = self.cassette.play_response(self._vcr_request)
            return VCRHTTPResponse(response)
        else:
            if self.cassette.write_protected and self.cassette.filter_request(self._vcr_request):
>               raise CannotOverwriteExistingCassetteException(
                    cassette=self.cassette,
                    failed_request=self._vcr_request,
                )
E               vcr.errors.CannotOverwriteExistingCassetteException: Can't overwrite existing cassette ('/mnt/vss/_work/1/s/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/recordings/test_webapp_ssl.yaml') in your current record mode ('once').
E               No match for the request (<Request (GET) https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.Web/sites/web-ssl-test000003/slots/slot-ssl-test000004?api-version=2024-11-01>)&nbsp;was&nbsp;found.
E               Found 7 similar requests with 0 different matcher(s) :
E               
E               1 - (<Request (GET) https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.Web/sites/web-ssl-test000003/slots/slot-ssl-test000004?api-version=2024-11-01>).
E               Matchers succeeded : ['method', 'scheme', 'host', 'port', 'path', '_custom_request_query_matcher']
E               Matchers failed :
E               
E               2 - (<Request (GET) https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.Web/sites/web-ssl-test000003/slots/slot-ssl-test000004?api-version=2024-11-01>).
E               Matchers succeeded : ['method', 'scheme', 'host', 'port', 'path', '_custom_request_query_matcher']
E               Matchers failed :
E               
E               3 - (<Request (GET) https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.Web/sites/web-ssl-test000003/slots/slot-ssl-test000004?api-version=2024-11-01>).
E               Matchers succeeded : ['method', 'scheme', 'host', 'port', 'path', '_custom_request_query_matcher']
E               Matchers failed :
E               
E               4 - (<Request (GET) https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.Web/sites/web-ssl-test000003/slots/slot-ssl-test000004?api-version=2024-11-01>).
E               Matchers succeeded : ['method', 'scheme', 'host', 'port', 'path', '_custom_request_query_matcher']
E               Matchers failed :
E               
E               5 - (<Request (GET) https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.Web/sites/web-ssl-test000003/slots/slot-ssl-test000004?api-version=2024-11-01>).
E               Matchers succeeded : ['method', 'scheme', 'host', 'port', 'path', '_custom_request_query_matcher']
E               Matchers failed :
E               
E               6 - (<Request (GET) https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.Web/sites/web-ssl-test000003/slots/slot-ssl-test000004?api-version=2024-11-01>).
E               Matchers succeeded : ['method', 'scheme', 'host', 'port', 'path', '_custom_request_query_matcher']
E               Matchers failed :
E               
E               7 - (<Request (GET) https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.Web/sites/web-ssl-test000003/slots/slot-ssl-test000004?api-version=2024-11-01>).
E               Matchers succeeded : ['method', 'scheme', 'host', 'port', 'path', '_custom_request_query_matcher']
E               Matchers failed :

env/lib/python3.13/site-packages/vcr/stubs/init.py:277: CannotOverwriteExistingCassetteException

During handling of the above exception, another exception occurred:

self = <azure.cli.command_modules.appservice.tests.latest.test_webapp_commands.WebappSSLCertTest testMethod=test_webapp_ssl>
resource_group = 'clitest.rg000001', resource_group_location = 'westeurope'

    @AllowLargeResponse()
    @ResourceGroupPreparer(location=WINDOWS_ASP_LOCATION_WEBAPP)
    def test_webapp_ssl(self, resource_group, resource_group_location):
        plan = self.create_random_name(prefix='ssl-test-plan', length=24)
        webapp_name = self.create_random_name(prefix='web-ssl-test', length=20)
        slot_name = self.create_random_name(prefix='slot-ssl-test', length=20)
        # Cert Generated using
        # https://learn.microsoft.com/azure/app-service-web/web-sites-configure-ssl-certificate#bkmk_ssopenssl
        pfx_file = os.path.join(TEST_DIR, 'server.pfx')
        cert_password = 'test'
        cert_thumbprint = '9E9735C45C792B03B3FFCCA614852B32EE71AD6B'
        # we configure tags here in a hope to capture a repro for #6929
        self.cmd(
            'appservice plan create -g {} -n {} --sku S1 --tags plan=plan1'.format(resource_group, plan))
        self.cmd('appservice plan show -g {} -n {}'.format(resource_group,
                                                           plan), self.check('tags.plan', 'plan1'))
        self.cmd('webapp create -g {} -n {} --plan {} --tags web=web1'.format(
            resource_group, webapp_name, plan))
        self.cmd('webapp config ssl upload -g {} -n {} --certificate-file "{}" --certificate-password {} --certificate-name {}'.format(resource_group, webapp_name, pfx_file, cert_password, "test123"), checks=[
            JMESPathCheck('thumbprint', cert_thumbprint),
            JMESPathCheck('name', 'test123')
        ])
        self.cmd('webapp show -g {} -n {}'.format(resource_group,
                                                  webapp_name), self.check('tags.web', 'web1'))
        self.cmd('webapp config ssl bind -g {} -n {} --certificate-thumbprint {} --ssl-type {}'.format(resource_group, webapp_name, cert_thumbprint, 'SNI'), checks=[
            JMESPathCheck("hostNameSslStates
[?name=='{}.azurewebsites.net']
️✔️aro
️✔️latest
️✔️3.12
️✔️3.13
️✔️backup
️✔️latest
️✔️3.12
️✔️3.13
️✔️batch
️✔️latest
️✔️3.12
️✔️3.13
️✔️batchai
️✔️latest
️✔️3.12
️✔️3.13
️✔️billing
️✔️latest
️✔️3.12
️✔️3.13
️✔️botservice
️✔️latest
️✔️3.12
️✔️3.13
️✔️cdn
️✔️latest
️✔️3.12
️✔️3.13
️✔️cloud
️✔️latest
️✔️3.12
️✔️3.13
️✔️cognitiveservices
️✔️latest
️✔️3.12
️✔️3.13
️✔️compute_recommender
️✔️latest
️✔️3.12
️✔️3.13
️✔️computefleet
️✔️latest
️✔️3.12
️✔️3.13
️✔️config
️✔️latest
️✔️3.12
️✔️3.13
️✔️configure
️✔️latest
️✔️3.12
️✔️3.13
️✔️consumption
️✔️latest
️✔️3.12
️✔️3.13
️✔️container
️✔️latest
️✔️3.12
️✔️3.13
️✔️containerapp
️✔️latest
️✔️3.12
️✔️3.13
️✔️core
️✔️latest
️✔️3.12
️✔️3.13
️✔️cosmosdb
️✔️latest
️✔️3.12
️✔️3.13
️✔️databoxedge
️✔️latest
️✔️3.12
️✔️3.13
️✔️dls
️✔️latest
️✔️3.12
️✔️3.13
️✔️dms
️✔️latest
️✔️3.12
️✔️3.13
️✔️eventgrid
️✔️latest
️✔️3.12
️✔️3.13
️✔️eventhubs
️✔️latest
️✔️3.12
️✔️3.13
️✔️feedback
️✔️latest
️✔️3.12
️✔️3.13
️✔️find
️✔️latest
️✔️3.12
️✔️3.13
️✔️hdinsight
️✔️latest
️✔️3.12
️✔️3.13
️✔️identity
️✔️latest
️✔️3.12
️✔️3.13
️✔️iot
️✔️latest
️✔️3.12
️✔️3.13
️✔️keyvault
️✔️latest
️✔️3.12
️✔️3.13
️✔️lab
️✔️latest
️✔️3.12
️✔️3.13
️✔️managedservices
️✔️latest
️✔️3.12
️✔️3.13
️✔️maps
️✔️latest
️✔️3.12
️✔️3.13
️✔️marketplaceordering
️✔️latest
️✔️3.12
️✔️3.13
️✔️monitor
️✔️latest
️✔️3.12
️✔️3.13
️✔️mysql
️✔️latest
️✔️3.12
️✔️3.13
️✔️netappfiles
️✔️latest
️✔️3.12
️✔️3.13
️✔️network
️✔️latest
️✔️3.12
️✔️3.13
️✔️policyinsights
️✔️latest
️✔️3.12
️✔️3.13
️✔️postgresql
️✔️latest
️✔️3.12
️✔️3.13
️✔️privatedns
️✔️latest
️✔️3.12
️✔️3.13
️✔️profile
️✔️latest
️✔️3.12
️✔️3.13
️✔️rdbms
️✔️latest
️✔️3.12
️✔️3.13
️✔️redis
️✔️latest
️✔️3.12
️✔️3.13
️✔️relay
️✔️latest
️✔️3.12
️✔️3.13
️✔️resource
️✔️latest
️✔️3.12
️✔️3.13
️✔️role
️✔️latest
️✔️3.12
️✔️3.13
️✔️search
️✔️latest
️✔️3.12
️✔️3.13
️✔️security
️✔️latest
️✔️3.12
️✔️3.13
️✔️servicebus
️✔️latest
️✔️3.12
️✔️3.13
️✔️serviceconnector
️✔️latest
️✔️3.12
️✔️3.13
️✔️servicefabric
️✔️latest
️✔️3.12
️✔️3.13
️✔️signalr
️✔️latest
️✔️3.12
️✔️3.13
️✔️sql
️✔️latest
️✔️3.12
️✔️3.13
️✔️sqlvm
️✔️latest
️✔️3.12
️✔️3.13
️✔️storage
️✔️latest
️✔️3.12
️✔️3.13
️✔️synapse
️✔️latest
️✔️3.12
️✔️3.13
️✔️telemetry
️✔️latest
️✔️3.12
️✔️3.13
️✔️util
️✔️latest
️✔️3.12
️✔️3.13
️✔️vm
️✔️latest
️✔️3.12
️✔️3.13

@azure-client-tools-bot-prd
Copy link

Hi @seligj95,
Since the current milestone time is less than 7 days, this pr will be reviewed in the next milestone.

@azure-client-tools-bot-prd
Copy link

azure-client-tools-bot-prd bot commented Mar 26, 2026

️✔️AzureCLI-BreakingChangeTest
️✔️Non Breaking Changes

@yonzhan
Copy link
Collaborator

yonzhan commented Mar 26, 2026

Thank you for your contribution! We will review the pull request and get back to you soon.

@github-actions
Copy link

The git hooks are available for azure-cli and azure-cli-extensions repos. They could help you run required checks before creating the PR.

Please sync the latest code with latest dev branch (for azure-cli) or main branch (for azure-cli-extensions).
After that please run the following commands to enable git hooks:

pip install azdev --upgrade
azdev setup -c <your azure-cli repo path> -r <your azure-cli-extensions repo path>

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes Azure Policy denial for az webapp config ssl bind by ensuring the update PUT includes policy-sensitive site fields (e.g., httpsOnly) rather than sending a minimal Site payload.

Changes:

  • Update _update_host_name_ssl_state to reuse the existing Site object instead of constructing a minimal one.
  • Add unit tests validating the full Site object is passed through and that the slot argument is forwarded.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
src/azure-cli/azure/cli/command_modules/appservice/custom.py Reuses the retrieved Site object when calling begin_create_or_update for SSL binding updates.
src/azure-cli/azure/cli/command_modules/appservice/tests/latest/test_webapp_commands_thru_mock.py Adds tests around _update_host_name_ssl_state behavior (full site passthrough + slot forwarding).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +6045 to +6051
HostNameSslState = cmd.get_models('HostNameSslState')
webapp.host_name_ssl_states = [HostNameSslState(name=host_name,
ssl_state=ssl_state,
thumbprint=thumbprint,
to_update=True)]
return _generic_site_operation(cmd.cli_ctx, resource_group_name, webapp_name, 'begin_create_or_update',
slot, updated_webapp)
slot, webapp)
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When slot is provided, this helper assumes the passed webapp object represents that slot. In the current call path (_update_ssl_binding fetches client.web_apps.get(...) regardless of slot), this can cause a slot PUT (begin_create_or_update_slot) to reuse the production Site payload (including policy-sensitive fields like https_only), potentially triggering policy denial or unintentionally overwriting slot-specific settings. Consider fetching the Site for the slot (client.web_apps.get_slot(...) or _generic_site_operation(..., 'get', slot)) and using that object for the update when slot is not None.

Copilot uses AI. Check for mistakes.
Comment on lines +692 to +693
# site is the 6th positional arg (index 5)
self.assertIs(call_args[0][5], webapp)
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

test_update_host_name_ssl_state_with_slot asserts object identity (assertIs(..., webapp)), which will fail if the implementation is adjusted to avoid in-place mutation or to refetch the correct slot Site (which is likely needed for correctness). It would be less brittle to assert on the payload’s relevant properties (e.g., https_only, location, tags) and/or add a focused test at the _update_ssl_binding level to ensure slot updates use get_slot-retrieved site data.

Suggested change
# site is the 6th positional arg (index 5)
self.assertIs(call_args[0][5], webapp)
# site is the 6th positional arg (index 5); verify it has the expected properties
site_arg = call_args[0][5]
self.assertEqual(site_arg.name, webapp.name)
self.assertEqual(site_arg.location, webapp.location)

Copilot uses AI. Check for mistakes.
- In _update_ssl_binding, use get_slot() when slot is provided to avoid
  sending production Site payload for slot updates (policy/settings issue)
- Replace assertIs with property assertions in slot test for robustness

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@yonzhan yonzhan assigned yanzhudd and unassigned zhoxing-ms Mar 26, 2026
@seligj95
Copy link
Contributor Author

Consolidated into #33058

@seligj95 seligj95 closed this Mar 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

"az webapp config ssl bind" command passes partial site object causing Azure policy enforcement to fail when it shouldn't

5 participants