Skip to content
This repository was archived by the owner on Mar 23, 2026. It is now read-only.

Commit 0eded68

Browse files
authored
KMS: Move Tagging Functionality into methods on the Provider (#13595)
1 parent d1407d6 commit 0eded68

3 files changed

Lines changed: 41 additions & 19 deletions

File tree

localstack-core/localstack/services/kms/models.py

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,8 @@
4949
UnsupportedOperationException,
5050
)
5151
from localstack.constants import TAG_KEY_CUSTOM_ID
52-
from localstack.services.kms.exceptions import TagException, ValidationException
53-
from localstack.services.kms.utils import is_valid_key_arn, validate_tag
52+
from localstack.services.kms.exceptions import ValidationException
53+
from localstack.services.kms.utils import is_valid_key_arn, validate_tag, validate_tag_list
5454
from localstack.services.stores import AccountRegionBundle, BaseStore, LocalAttribute
5555
from localstack.utils.aws.arns import get_partition, kms_alias_arn, kms_key_arn
5656
from localstack.utils.crypto import decrypt, encrypt
@@ -630,12 +630,7 @@ def add_tags(self, tags: TagList) -> None:
630630
if not tags:
631631
return
632632

633-
unique_tag_keys = {tag["TagKey"] for tag in tags}
634-
if len(unique_tag_keys) < len(tags):
635-
raise TagException("Duplicate tag keys")
636-
637-
if len(tags) > 50:
638-
raise TagException("Too many tags")
633+
validate_tag_list(tags)
639634

640635
# Do not care if we overwrite an existing tag:
641636
# https://docs.aws.amazon.com/kms/latest/APIReference/API_TagResource.html

localstack-core/localstack/services/kms/provider.py

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,9 @@
106106
ScheduleKeyDeletionResponse,
107107
SignRequest,
108108
SignResponse,
109+
Tag,
110+
TagKeyList,
111+
TagList,
109112
TagResourceRequest,
110113
UnsupportedOperationException,
111114
UntagResourceRequest,
@@ -389,6 +392,20 @@ def _parse_key_id(key_id_or_arn: str, context: RequestContext) -> tuple[str, str
389392
def _is_rsa_spec(key_spec: str) -> bool:
390393
return key_spec in [KeySpec.RSA_2048, KeySpec.RSA_3072, KeySpec.RSA_4096]
391394

395+
# These tagging methods are overwritten in the Pro implementation.
396+
def _get_key_tags(self, key: KmsKey, account_id: str, region: str) -> TagList:
397+
return [Tag(TagKey=key, TagValue=value) for key, value in key.tags.items()]
398+
399+
def _set_key_tags(self, key: KmsKey, account_id: str, region: str, tags: TagList) -> None:
400+
return
401+
402+
def _remove_key_tags(
403+
self, key: KmsKey, account_id: str, region: str, tag_keys: TagKeyList
404+
) -> None:
405+
# AWS doesn't seem to mind removal of a non-existent tag, so we do not raise any exception.
406+
for tag_key in tag_keys:
407+
key.tags.pop(tag_key, None)
408+
392409
#
393410
# Operation Handlers
394411
#
@@ -400,6 +417,7 @@ def create_key(
400417
request: CreateKeyRequest = None,
401418
) -> CreateKeyResponse:
402419
key = self._create_kms_key(context.account_id, context.region, request)
420+
self._set_key_tags(key, context.account_id, context.region, request.get("Tags", []))
403421
return CreateKeyResponse(KeyMetadata=key.metadata)
404422

405423
@handler("ScheduleKeyDeletion", expand=False)
@@ -1466,15 +1484,15 @@ def list_resource_tags(
14661484
key = self._get_kms_key(
14671485
context.account_id, context.region, request.get("KeyId"), any_key_state_allowed=True
14681486
)
1469-
keys_list = PaginatedList(
1470-
[{"TagKey": tag_key, "TagValue": tag_value} for tag_key, tag_value in key.tags.items()]
1471-
)
1487+
keys_list = PaginatedList(self._get_key_tags(key, context.account_id, context.region))
14721488
page, next_token = keys_list.get_page(
14731489
lambda tag: tag.get("TagKey"),
14741490
next_token=request.get("Marker"),
14751491
page_size=request.get("Limit", 50),
14761492
)
1477-
kwargs = {"NextMarker": next_token, "Truncated": True} if next_token else {}
1493+
kwargs = (
1494+
{"NextMarker": next_token, "Truncated": True} if next_token else {"Truncated": False}
1495+
)
14781496
return ListResourceTagsResponse(Tags=page, **kwargs)
14791497

14801498
@handler("RotateKeyOnDemand", expand=False)
@@ -1498,29 +1516,30 @@ def rotate_key_on_demand(
14981516

14991517
@handler("TagResource", expand=False)
15001518
def tag_resource(self, context: RequestContext, request: TagResourceRequest) -> None:
1519+
tags = request["Tags"]
15011520
key = self._get_kms_key(
15021521
context.account_id,
15031522
context.region,
15041523
request.get("KeyId"),
15051524
enabled_key_allowed=True,
15061525
disabled_key_allowed=True,
15071526
)
1508-
key.add_tags(request.get("Tags"))
1527+
key.add_tags(tags)
1528+
self._set_key_tags(key, context.account_id, context.region, tags)
15091529

15101530
@handler("UntagResource", expand=False)
15111531
def untag_resource(self, context: RequestContext, request: UntagResourceRequest) -> None:
1532+
if not (tag_keys := request.get("TagKeys", [])):
1533+
return
1534+
15121535
key = self._get_kms_key(
15131536
context.account_id,
15141537
context.region,
15151538
request.get("KeyId"),
15161539
enabled_key_allowed=True,
15171540
disabled_key_allowed=True,
15181541
)
1519-
if not request.get("TagKeys"):
1520-
return
1521-
for tag_key in request.get("TagKeys"):
1522-
# AWS doesn't seem to mind removal of a non-existent tag, so we do not raise any exception.
1523-
key.tags.pop(tag_key, None)
1542+
self._remove_key_tags(key, context.account_id, context.region, tag_keys)
15241543

15251544
def derive_shared_secret(
15261545
self,

localstack-core/localstack/services/kms/utils.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import re
22
from collections.abc import Callable
33

4-
from localstack.aws.api.kms import DryRunOperationException, Tag, TagException
4+
from localstack.aws.api.kms import DryRunOperationException, Tag, TagException, TagList
55
from localstack.services.kms.exceptions import ValidationException
66
from localstack.utils.aws.arns import ARN_PARTITION_REGEX
77

@@ -60,6 +60,14 @@ def validate_tag(tag_position: int, tag: Tag) -> None:
6060
raise TagException("Tags beginning with aws: are reserved")
6161

6262

63+
def validate_tag_list(tag_list: TagList) -> None:
64+
unique_tag_keys = {tag["TagKey"] for tag in tag_list}
65+
if len(unique_tag_keys) < len(tag_list):
66+
raise TagException("Duplicate tag keys")
67+
if len(tag_list) > 50:
68+
raise TagException("Too many tags")
69+
70+
6371
def execute_dry_run_capable[T](func: Callable[..., T], dry_run: bool, *args, **kwargs) -> T:
6472
"""
6573
Executes a function unless dry run mode is enabled.

0 commit comments

Comments
 (0)