Skip to content

Commit f463e0b

Browse files
authored
[Key Vault] Add support for key rotation (Azure#20416)
1 parent 3f2a011 commit f463e0b

13 files changed

+1080
-12
lines changed

sdk/keyvault/azure-keyvault-keys/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
33
## 4.5.0b4 (Unreleased)
44

55
### Features Added
6+
- Added support for automated and on-demand key rotation in Azure Key Vault
7+
([#19840](https://github.com/Azure/azure-sdk-for-python/issues/19840))
8+
- Added `KeyClient.rotate_key` to rotate a key on-demand
9+
- Added `KeyClient.update_key_rotation_policy` to update a key's automated rotation policy
610

711
### Breaking Changes
812

sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/__init__.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@
22
# Copyright (c) Microsoft Corporation.
33
# Licensed under the MIT License.
44
# -------------------------------------
5-
from ._enums import KeyCurveName, KeyExportEncryptionAlgorithm, KeyOperation, KeyType
5+
from ._enums import KeyCurveName, KeyExportEncryptionAlgorithm, KeyOperation, KeyRotationPolicyAction, KeyType
66
from ._shared.client_base import ApiVersion
77
from ._models import (
88
DeletedKey,
99
JsonWebKey,
1010
KeyProperties,
1111
KeyReleasePolicy,
12+
KeyRotationLifetimeAction,
13+
KeyRotationPolicy,
1214
KeyVaultKey,
1315
KeyVaultKeyIdentifier,
1416
RandomBytes,
@@ -25,10 +27,13 @@
2527
"KeyCurveName",
2628
"KeyExportEncryptionAlgorithm",
2729
"KeyOperation",
30+
"KeyRotationPolicyAction",
2831
"KeyType",
2932
"DeletedKey",
3033
"KeyProperties",
3134
"KeyReleasePolicy",
35+
"KeyRotationLifetimeAction",
36+
"KeyRotationPolicy",
3237
"RandomBytes",
3338
"ReleaseKeyResult",
3439
]

sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_client.py

Lines changed: 70 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from ._shared import KeyVaultClientBase
99
from ._shared.exceptions import error_map as _error_map
1010
from ._shared._polling import DeleteRecoverPollingMethod, KeyVaultOperationPoller
11-
from ._models import DeletedKey, KeyVaultKey, KeyProperties, RandomBytes, ReleaseKeyResult
11+
from ._models import DeletedKey, KeyVaultKey, KeyProperties, KeyRotationPolicy, RandomBytes, ReleaseKeyResult
1212

1313
try:
1414
from typing import TYPE_CHECKING
@@ -17,7 +17,7 @@
1717

1818
if TYPE_CHECKING:
1919
# pylint:disable=unused-import
20-
from typing import Any, Optional, Union
20+
from typing import Any, Iterable, Optional, Union
2121
from azure.core.paging import ItemPaged
2222
from azure.core.polling import LROPoller
2323
from ._models import JsonWebKey
@@ -45,7 +45,7 @@ class KeyClient(KeyVaultClientBase):
4545
:dedent: 4
4646
"""
4747

48-
# pylint:disable=protected-access
48+
# pylint:disable=protected-access, too-many-public-methods
4949

5050
def _get_attributes(self, enabled, not_before, expires_on, exportable=None):
5151
"""Return a KeyAttributes object if none-None attributes are provided, or None otherwise"""
@@ -727,3 +727,70 @@ def get_random_bytes(self, count, **kwargs):
727727
parameters = self._models.GetRandomBytesRequest(count=count)
728728
result = self._client.get_random_bytes(vault_base_url=self._vault_url, parameters=parameters, **kwargs)
729729
return RandomBytes(value=result.value)
730+
731+
@distributed_trace
732+
def get_key_rotation_policy(self, name, **kwargs):
733+
# type: (str, **Any) -> KeyRotationPolicy
734+
"""Get the rotation policy of a Key Vault key.
735+
736+
:param str name: The name of the key.
737+
738+
:return: The key rotation policy.
739+
:rtype: ~azure.keyvault.keys.KeyRotationPolicy
740+
:raises: :class: `~azure.core.exceptions.HttpResponseError`
741+
"""
742+
policy = self._client.get_key_rotation_policy(vault_base_url=self._vault_url, key_name=name, **kwargs)
743+
return KeyRotationPolicy._from_generated(policy)
744+
745+
@distributed_trace
746+
def rotate_key(self, name, **kwargs):
747+
# type: (str, **Any) -> KeyVaultKey
748+
"""Rotate the key based on the key policy by generating a new version of the key.
749+
750+
This operation requires the keys/rotate permission.
751+
752+
:param str name: The name of the key to rotate.
753+
754+
:return: The new version of the rotated key.
755+
:rtype: ~azure.keyvault.keys.KeyVaultKey
756+
:raises: :class:`~azure.core.exceptions.HttpResponseError`
757+
"""
758+
bundle = self._client.rotate_key(vault_base_url=self._vault_url, key_name=name, **kwargs)
759+
return KeyVaultKey._from_key_bundle(bundle)
760+
761+
@distributed_trace
762+
def update_key_rotation_policy(self, name, **kwargs):
763+
# type: (str, **Any) -> KeyRotationPolicy
764+
"""Updates the rotation policy of a Key Vault key.
765+
766+
This operation requires the keys/update permission.
767+
768+
:param str name: The name of the key in the given vault.
769+
770+
:keyword lifetime_actions: Actions that will be performed by Key Vault over the lifetime of a key.
771+
:paramtype lifetime_actions: Iterable[~azure.keyvault.keys.KeyRotationLifetimeAction]
772+
:keyword str expires_in: The expiry time of the policy that will be applied on new key versions, defined as an
773+
ISO 8601 duration. For example: 90 days is "P90D", 3 months is "P3M", and 48 hours is "PT48H".
774+
775+
:return: The updated rotation policy.
776+
:rtype: ~azure.keyvault.keys.KeyRotationPolicy
777+
:raises: :class:`~azure.core.exceptions.HttpResponseError`
778+
"""
779+
lifetime_actions = kwargs.pop("lifetime_actions", None)
780+
if lifetime_actions:
781+
lifetime_actions = [
782+
self._models.LifetimeActions(
783+
action=self._models.LifetimeActionsType(type=action.action),
784+
trigger=self._models.LifetimeActionsTrigger(
785+
time_after_create=action.time_after_create, time_before_expiry=action.time_before_expiry
786+
),
787+
)
788+
for action in lifetime_actions
789+
]
790+
791+
attributes = self._models.KeyRotationPolicyAttributes(expiry_time=kwargs.pop("expires_in", None))
792+
policy = self._models.KeyRotationPolicy(lifetime_actions=lifetime_actions, attributes=attributes)
793+
result = self._client.update_key_rotation_policy(
794+
vault_base_url=self._vault_url, key_name=name, key_rotation_policy=policy
795+
)
796+
return KeyRotationPolicy._from_generated(result)

sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_enums.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,13 @@ class KeyOperation(with_metaclass(CaseInsensitiveEnumMeta, str, Enum)):
3939
export = "export"
4040

4141

42+
class KeyRotationPolicyAction(with_metaclass(CaseInsensitiveEnumMeta, str, Enum)):
43+
"""The action that will be executed in a key rotation policy"""
44+
45+
ROTATE = "Rotate" #: Rotate the key based on the key policy.
46+
NOTIFY = "Notify" #: Trigger Event Grid events.
47+
48+
4249
class KeyType(with_metaclass(CaseInsensitiveEnumMeta, str, Enum)):
4350
"""Supported key types"""
4451

sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_models.py

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
from typing import Any, Dict, Optional, List
1717
from datetime import datetime
1818
from ._generated.v7_0 import models as _models
19-
from ._enums import KeyOperation, KeyType
19+
from ._enums import KeyOperation, KeyRotationPolicyAction, KeyType
2020

2121
KeyOperationResult = namedtuple("KeyOperationResult", ["id", "value"])
2222

@@ -279,6 +279,71 @@ def __init__(self, value):
279279
self.value = value
280280

281281

282+
class KeyRotationLifetimeAction(object):
283+
"""An action and its corresponding trigger that will be performed by Key Vault over the lifetime of a key.
284+
285+
:param action: The action that will be executed.
286+
:type action: ~azure.keyvault.keys.KeyRotationPolicyAction or str
287+
288+
:keyword str time_after_create: Time after creation to attempt the specified action, as an ISO 8601 duration.
289+
For example, 90 days is "P90D".
290+
:keyword str time_before_expiry: Time before expiry to attempt the specified action, as an ISO 8601 duration.
291+
For example, 90 days is "P90D".
292+
"""
293+
294+
def __init__(self, action, **kwargs):
295+
# type: (KeyRotationPolicyAction, **Any) -> None
296+
self.action = action
297+
self.time_after_create = kwargs.get("time_after_create", None)
298+
self.time_before_expiry = kwargs.get("time_before_expiry", None)
299+
300+
@classmethod
301+
def _from_generated(cls, lifetime_action):
302+
if lifetime_action.trigger:
303+
return cls(
304+
action=lifetime_action.action.type,
305+
time_after_create=lifetime_action.trigger.time_after_create,
306+
time_before_expiry=lifetime_action.trigger.time_before_expiry,
307+
)
308+
return cls(action=lifetime_action.action)
309+
310+
311+
class KeyRotationPolicy(object):
312+
"""The key rotation policy that belongs to a key.
313+
314+
:ivar str id: The identifier of the key rotation policy.
315+
:ivar lifetime_actions: Actions that will be performed by Key Vault over the lifetime of a key.
316+
:type lifetime_actions: list[~azure.keyvault.keys.KeyRotationLifetimeAction]
317+
:ivar str expires_in: The expiry time of the policy that will be applied on new key versions, defined as an ISO
318+
8601 duration. For example, 90 days is "P90D".
319+
:ivar created_on: When the policy was created, in UTC
320+
:type created_on: ~datetime.datetime
321+
:ivar updated_on: When the policy was last updated, in UTC
322+
:type updated_on: ~datetime.datetime
323+
"""
324+
325+
def __init__(self, policy_id, **kwargs):
326+
# type: (str, **Any) -> None
327+
self.id = policy_id
328+
self.lifetime_actions = kwargs.get("lifetime_actions", None)
329+
self.expires_in = kwargs.get("expires_in", None)
330+
self.created_on = kwargs.get("created_on", None)
331+
self.updated_on = kwargs.get("updated_on", None)
332+
333+
@classmethod
334+
def _from_generated(cls, policy):
335+
lifetime_actions = [KeyRotationLifetimeAction._from_generated(action) for action in policy.lifetime_actions] # pylint:disable=protected-access
336+
if policy.attributes:
337+
return cls(
338+
policy_id=policy.id,
339+
lifetime_actions=lifetime_actions,
340+
expires_on=policy.attributes.expiry_time,
341+
created_on=policy.attributes.created,
342+
updated_on=policy.attributes.updated,
343+
)
344+
return cls(policy_id=policy.id, lifetime_actions=lifetime_actions)
345+
346+
282347
class KeyVaultKey(object):
283348
"""A key's attributes and cryptographic material.
284349

sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/aio/_client.py

Lines changed: 75 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,20 @@
1111
from .._shared._polling_async import AsyncDeleteRecoverPollingMethod
1212
from .._shared import AsyncKeyVaultClientBase
1313
from .._shared.exceptions import error_map as _error_map
14-
from .. import DeletedKey, JsonWebKey, KeyProperties, KeyVaultKey, RandomBytes, ReleaseKeyResult
14+
from .. import (
15+
DeletedKey,
16+
JsonWebKey,
17+
KeyProperties,
18+
KeyRotationPolicy,
19+
KeyVaultKey,
20+
RandomBytes,
21+
ReleaseKeyResult,
22+
)
1523

1624
if TYPE_CHECKING:
1725
# pylint:disable=ungrouped-imports
1826
from azure.core.async_paging import AsyncItemPaged
19-
from typing import Any, Optional, Union
27+
from typing import Any, Iterable, Optional, Union
2028
from .. import KeyType
2129

2230

@@ -42,7 +50,7 @@ class KeyClient(AsyncKeyVaultClientBase):
4250
:dedent: 4
4351
"""
4452

45-
# pylint:disable=protected-access
53+
# pylint:disable=protected-access, too-many-public-methods
4654

4755
def _get_attributes(self, enabled, not_before, expires_on, exportable=None):
4856
"""Return a KeyAttributes object if none-None attributes are provided, or None otherwise"""
@@ -702,3 +710,67 @@ async def get_random_bytes(self, count: int, **kwargs: "Any") -> RandomBytes:
702710
parameters = self._models.GetRandomBytesRequest(count=count)
703711
result = await self._client.get_random_bytes(vault_base_url=self._vault_url, parameters=parameters, **kwargs)
704712
return RandomBytes(value=result.value)
713+
714+
@distributed_trace_async
715+
async def get_key_rotation_policy(self, name: str, **kwargs: "Any") -> "KeyRotationPolicy":
716+
"""Get the rotation policy of a Key Vault key.
717+
718+
:param str name: The name of the key.
719+
720+
:return: The key rotation policy.
721+
:rtype: ~azure.keyvault.keys.KeyRotationPolicy
722+
:raises: :class:`~azure.core.exceptions.HttpResponseError`
723+
"""
724+
policy = await self._client.get_key_rotation_policy(vault_base_url=self._vault_url, key_name=name, **kwargs)
725+
return KeyRotationPolicy._from_generated(policy)
726+
727+
@distributed_trace_async
728+
async def rotate_key(self, name: str, **kwargs: "Any") -> KeyVaultKey:
729+
"""Rotate the key based on the key policy by generating a new version of the key.
730+
731+
This operation requires the keys/rotate permission.
732+
733+
:param str name: The name of the key to rotate.
734+
735+
:return: The new version of the rotated key.
736+
:rtype: ~azure.keyvault.keys.KeyVaultKey
737+
:raises: :class:`~azure.core.exceptions.HttpResponseError`
738+
"""
739+
bundle = await self._client.rotate_key(vault_base_url=self._vault_url, key_name=name, **kwargs)
740+
return KeyVaultKey._from_key_bundle(bundle)
741+
742+
@distributed_trace_async
743+
async def update_key_rotation_policy(self, name: str, **kwargs: "Any") -> KeyRotationPolicy:
744+
"""Updates the rotation policy of a Key Vault key.
745+
746+
This operation requires the keys/update permission.
747+
748+
:param str name: The name of the key in the given vault.
749+
750+
:keyword lifetime_actions: Actions that will be performed by Key Vault over the lifetime of a key.
751+
:paramtype lifetime_actions: Iterable[~azure.keyvault.keys.KeyRotationLifetimeAction]
752+
:keyword str expires_in: The expiry time of the policy that will be applied on new key versions, defined as an
753+
ISO 8601 duration. For example: 90 days is "P90D", 3 months is "P3M", and 48 hours is "PT48H".
754+
755+
:return: The updated rotation policy.
756+
:rtype: ~azure.keyvault.keys.KeyRotationPolicy
757+
:raises: :class:`~azure.core.exceptions.HttpResponseError`
758+
"""
759+
lifetime_actions = kwargs.pop("lifetime_actions", None)
760+
if lifetime_actions:
761+
lifetime_actions = [
762+
self._models.LifetimeActions(
763+
action=self._models.LifetimeActionsType(type=action.action),
764+
trigger=self._models.LifetimeActionsTrigger(
765+
time_after_create=action.time_after_create, time_before_expiry=action.time_before_expiry
766+
),
767+
)
768+
for action in lifetime_actions
769+
]
770+
771+
attributes = self._models.KeyRotationPolicyAttributes(expiry_time=kwargs.pop("expires_in", None))
772+
policy = self._models.KeyRotationPolicy(lifetime_actions=lifetime_actions, attributes=attributes)
773+
result = await self._client.update_key_rotation_policy(
774+
vault_base_url=self._vault_url, key_name=name, key_rotation_policy=policy
775+
)
776+
return KeyRotationPolicy._from_generated(result)

0 commit comments

Comments
 (0)