Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion sdk/keyvault/azure-keyvault-administration/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,15 @@

### Breaking Changes

### Bugs Fixed
- Changed the continuation token format. Continuation tokens generated by previous versions of
`azure-keyvault-administration` are not compatible with this version. Similarly, continuation tokens generated by
previous versions of this library are not compatible with versions of `azure-core>=1.38.0`.

### Bugs Fixed
### Other Changes

- Updated minimum `azure-core` version to 1.38.0

## 4.6.0 (2025-06-16)

### Features Added
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@
# ------------------------------------
import base64
import functools
import pickle
import json
from typing import Any, Callable, Optional, overload
from urllib.parse import urlparse

from typing_extensions import Literal

from azure.core.pipeline import PipelineResponse
from azure.core.polling import LROPoller
from azure.core.tracing.decorator import distributed_trace

Expand All @@ -19,12 +20,61 @@
from ._internal.polling import KeyVaultBackupClientPolling, KeyVaultBackupClientPollingMethod


def _parse_status_url(url):
def _parse_status_url(url: str) -> str:
parsed = urlparse(url)
job_id = parsed.path.split("/")[2]
return job_id


def _get_continuation_token(pipeline_response: PipelineResponse) -> str:
"""Returns an opaque token which can be used by the user to rehydrate/restart the LRO.

Saves the state of the LRO based on a status response so that polling can be resumed from that context. Because
the service has different operations for backup/restore starting vs. status checking, the caller is expected to
first use the status URL from the initial response to make a status request and then pass that status response to
this function to be serialized into a continuation token.

:param pipeline_response: The pipeline response of the operation status request.
:type pipeline_response: ~azure.core.pipeline.PipelineResponse
:returns: An opaque continuation token that can be provided to Core to rehydrate the LRO.
:rtype: str
"""
# Headers needed for LRO rehydration - use an allowlist approach for security
lro_headers = {"azure-asyncoperation", "operation-location", "location", "content-type", "retry-after"}
response = pipeline_response.http_response
filtered_headers = {k: v for k, v in response.headers.items() if k.lower() in lro_headers}

request = response.request
# Serialize the essential parts of the PipelineResponse to JSON.
if request:
request_headers = {}
# Preserve x-ms-client-request-id for request correlation
if "x-ms-client-request-id" in request.headers:
request_headers["x-ms-client-request-id"] = request.headers["x-ms-client-request-id"]
request_state = {
"method": request.method,
"url": request.url,
"headers": request_headers,
}
else:
request_state = None

# Use a versioned token schema: {"version": <int>, "data": <your state>}
# This allows for future compatibility checking when deserializing
token = {
"version": 1,
"data": {
"request": request_state,
"response": {
"status_code": response.status_code,
"headers": filtered_headers,
"content": base64.b64encode(response.content).decode("ascii"),
},
},
}
return base64.b64encode(json.dumps(token).encode("utf-8")).decode("ascii")


class KeyVaultBackupClient(KeyVaultClientBase):
"""Performs Key Vault backup and restore operations.

Expand Down Expand Up @@ -56,7 +106,7 @@ def _use_continuation_token(self, continuation_token: str, status_method: Callab
)
if "azure-asyncoperation" not in pipeline_response.http_response.headers:
pipeline_response.http_response.headers["azure-asyncoperation"] = status_url
return base64.b64encode(pickle.dumps(pipeline_response)).decode("ascii")
return _get_continuation_token(pipeline_response)

@overload
def begin_backup(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
class KeyVaultBackupClientPolling(OperationResourcePolling):
def __init__(self) -> None:
self._polling_url = ""
super(KeyVaultBackupClientPolling, self).__init__(operation_location_header="azure-asyncoperation")
super().__init__(operation_location_header="azure-asyncoperation")

def get_polling_url(self) -> str:
return self._polling_url
Expand All @@ -34,4 +34,13 @@ def set_initial_status(self, pipeline_response: "PipelineResponse") -> str:

class KeyVaultBackupClientPollingMethod(LROBasePolling):
def get_continuation_token(self) -> str:
"""
Get a continuation token to resume the polling later.

:return: A continuation token.
:rtype: str
"""
# Because of the operation structure, we need to use a "continuation token" that is just the status URL.
# This URL can then be used to fetch the status of the operation when resuming, at which point a genuine
# continuation token will be created from the response and provided to Core.
return base64.b64encode(self._operation.get_polling_url().encode()).decode("ascii")
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
# ------------------------------------
import base64
import functools
import pickle
from typing import Any, Callable, Optional, overload

from typing_extensions import Literal
Expand All @@ -13,7 +12,7 @@
from azure.core.tracing.decorator_async import distributed_trace_async

from .._generated.models import PreBackupOperationParameters, PreRestoreOperationParameters, SASTokenParameter
from .._backup_client import _parse_status_url
from .._backup_client import _get_continuation_token, _parse_status_url
from .._internal import AsyncKeyVaultClientBase, parse_folder_url
from .._internal.async_polling import KeyVaultAsyncBackupClientPollingMethod
from .._internal.polling import KeyVaultBackupClientPolling
Expand Down Expand Up @@ -51,7 +50,7 @@ async def _use_continuation_token(self, continuation_token: str, status_method:
)
if "azure-asyncoperation" not in pipeline_response.http_response.headers:
pipeline_response.http_response.headers["azure-asyncoperation"] = status_url
return base64.b64encode(pickle.dumps(pipeline_response)).decode("ascii")
return _get_continuation_token(pipeline_response)

@overload
async def begin_backup(
Expand Down
2 changes: 1 addition & 1 deletion sdk/keyvault/azure-keyvault-administration/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@
),
install_requires=[
"isodate>=0.6.1",
"azure-core>=1.31.0",
"azure-core>=1.38.0",
"typing-extensions>=4.6.0",
],
python_requires=">=3.9",
Expand Down