Repository: getmoto/moto
Pull request #6959 (2023-10-27 21:20:58)

Signer: Add tagging methods
New methods:
 - list_tags_by_resource()
 - tag_resource()
 - untag_resource()

The `put_signing_profile()`-method now also supports the `SigningMaterial`-parameter

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/getmoto/moto/pull/6959?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) Report
> Merging [#6959](https://app.codecov.io/gh/getmoto/moto/pull/6959?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) (4f58288) into [master](https://app.codecov.io/gh/getmoto/moto/commit/9df845cc1bcc4ca82f8b9c0c8382f5242d1e18ed?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) (9df845c) will **decrease** coverage by `0.01%`.
> Report is 3 commits behind head on master.
> The diff coverage is `82.92%`.

```diff
@@            Coverage Diff             @@
##           master    #6959      +/-   ##
==========================================
- Coverage   95.83%   95.83%   -0.01%     
==========================================
  Files         830      830              
  Lines       81348    81390      +42     
==========================================
+ Hits        77962    78000      +38     
- Misses       3386     3390       +4     
```

| [Flag](https://app.codecov.io/gh/getmoto/moto/pull/6959/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) | Coverage Δ | |
|---|---|---|
| [servertests](https://app.codecov.io/gh/getmoto/moto/pull/6959/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) | `36.65% <12.19%> (-0.01%)` | :arrow_down: |
| [unittests](https://app.codecov.io/gh/getmoto/moto/pull/6959/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) | `95.77% <82.92%> (-0.01%)` | :arrow_down: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Files](https://app.codecov.io/gh/getmoto/moto/pull/6959?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) | Coverage Δ | |
|---|---|---|
| [moto/signer/models.py](https://app.codecov.io/gh/getmoto/moto/pull/6959?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto#diff-bW90by9zaWduZXIvbW9kZWxzLnB5) | `100.00% <100.00%> (ø)` | |
| [moto/signer/urls.py](https://app.codecov.io/gh/getmoto/moto/pull/6959?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto#diff-bW90by9zaWduZXIvdXJscy5weQ==) | `100.00% <100.00%> (ø)` | |
| [moto/signer/responses.py](https://app.codecov.io/gh/getmoto/moto/pull/6959?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto#diff-bW90by9zaWduZXIvcmVzcG9uc2VzLnB5) | `86.53% <72.00%> (-13.47%)` | :arrow_down: |

... and [3 files with indirect coverage changes](https://app.codecov.io/gh/getmoto/moto/pull/6959/indirect-changes?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto)



Patch:
diff --git a/IMPLEMENTATION_COVERAGE.md b/IMPLEMENTATION_COVERAGE.md
index b82cc8b94..b35b2f297 100644
--- a/IMPLEMENTATION_COVERAGE.md
+++ b/IMPLEMENTATION_COVERAGE.md
@@ -6915,7 +6915,7 @@
 
 ## signer
 <details>
-<summary>21% implemented</summary>
+<summary>36% implemented</summary>
 
 - [ ] add_profile_permission
 - [X] cancel_signing_profile
@@ -6927,15 +6927,15 @@
 - [ ] list_signing_jobs
 - [X] list_signing_platforms
 - [ ] list_signing_profiles
-- [ ] list_tags_for_resource
+- [X] list_tags_for_resource
 - [X] put_signing_profile
 - [ ] remove_profile_permission
 - [ ] revoke_signature
 - [ ] revoke_signing_profile
 - [ ] sign_payload
 - [ ] start_signing_job
-- [ ] tag_resource
-- [ ] untag_resource
+- [X] tag_resource
+- [X] untag_resource
 </details>
 
 ## sns
diff --git a/README.md b/README.md
index aa56d3585..481e45058 100644
--- a/README.md
+++ b/README.md
@@ -60,7 +60,7 @@ def test_my_model_save():
     assert body == "is awesome"
 ```
 
-With the decorator wrapping the test, all the calls to s3 are automatically mocked out. The mock keeps the state of the buckets and keys.
+With the decorator wrapping the test, all the calls to s3 are automatically mocked out. The mock keeps track of the state of the buckets and keys.
 
 For a full list of which services and features are covered, please see our [implementation coverage](https://github.com/getmoto/moto/blob/master/IMPLEMENTATION_COVERAGE.md).
 
@@ -72,7 +72,7 @@ The full documentation can be found here:
 
 
 ### Financial Contributions
-Support this project and continued development, by sponsoring us!
+Support this project and its continued development, by sponsoring us!
 
 Click the `Sponsor`-button at the top of the page for more information.
 
diff --git a/docs/docs/services/signer.rst b/docs/docs/services/signer.rst
index 1cd8e9ed5..f74807b8a 100644
--- a/docs/docs/services/signer.rst
+++ b/docs/docs/services/signer.rst
@@ -41,10 +41,10 @@ signer
         
 
 - [ ] list_signing_profiles
-- [ ] list_tags_for_resource
+- [X] list_tags_for_resource
 - [X] put_signing_profile
   
-        The following parameters are not yet implemented: SigningMaterial, Overrides, SigningParamaters
+        The following parameters are not yet implemented: Overrides, SigningParameters
         
 
 - [ ] remove_profile_permission
@@ -52,6 +52,6 @@ signer
 - [ ] revoke_signing_profile
 - [ ] sign_payload
 - [ ] start_signing_job
-- [ ] tag_resource
-- [ ] untag_resource
+- [X] tag_resource
+- [X] untag_resource
 
diff --git a/moto/signer/models.py b/moto/signer/models.py
index 59395db54..6d6e69bec 100644
--- a/moto/signer/models.py
+++ b/moto/signer/models.py
@@ -2,17 +2,17 @@ from typing import Any, Dict, List, Optional
 
 from moto.core import BaseBackend, BackendDict, BaseModel
 from moto.moto_api._internal import mock_random
+from moto.utilities.tagging_service import TaggingService
 
 
 class SigningProfile(BaseModel):
     def __init__(
         self,
-        account_id: str,
-        region: str,
+        backend: "SignerBackend",
         name: str,
         platform_id: str,
+        signing_material: Dict[str, str],
         signature_validity_period: Optional[Dict[str, Any]],
-        tags: Dict[str, str],
     ):
         self.name = name
         self.platform_id = platform_id
@@ -20,12 +20,13 @@ class SigningProfile(BaseModel):
             "value": 135,
             "type": "MONTHS",
         }
-        self.tags = tags
+        self.backend = backend
 
         self.status = "Active"
-        self.arn = f"arn:aws:signer:{region}:{account_id}:/signing-profiles/{name}"
+        self.arn = f"arn:aws:signer:{backend.region_name}:{backend.account_id}:/signing-profiles/{name}"
         self.profile_version = mock_random.get_random_hex(10)
         self.profile_version_arn = f"{self.arn}/{self.profile_version}"
+        self.signing_material = signing_material
 
     def cancel(self) -> None:
         self.status = "Canceled"
@@ -43,7 +44,7 @@ class SigningProfile(BaseModel):
                     "profileName": self.name,
                     "platformId": self.platform_id,
                     "signatureValidityPeriod": self.signature_validity_period,
-                    "signingMaterial": {},
+                    "signingMaterial": self.signing_material,
                     "platformDisplayName": next(
                         (
                             p["displayName"]
@@ -54,8 +55,9 @@ class SigningProfile(BaseModel):
                     ),
                 }
             )
-            if self.tags:
-                small.update({"tags": self.tags})
+            tags = self.backend.list_tags_for_resource(self.arn)
+            if tags:
+                small.update({"tags": tags})
         return small
 
 
@@ -160,6 +162,7 @@ class SignerBackend(BaseBackend):
     def __init__(self, region_name: str, account_id: str):
         super().__init__(region_name, account_id)
         self.signing_profiles: Dict[str, SigningProfile] = dict()
+        self.tagger = TaggingService()
 
     def cancel_signing_profile(self, profile_name: str) -> None:
         self.signing_profiles[profile_name].cancel()
@@ -172,20 +175,21 @@ class SignerBackend(BaseBackend):
         profile_name: str,
         signature_validity_period: Optional[Dict[str, Any]],
         platform_id: str,
+        signing_material: Dict[str, str],
         tags: Dict[str, str],
     ) -> SigningProfile:
         """
-        The following parameters are not yet implemented: SigningMaterial, Overrides, SigningParamaters
+        The following parameters are not yet implemented: Overrides, SigningParameters
         """
         profile = SigningProfile(
-            account_id=self.account_id,
-            region=self.region_name,
+            backend=self,
             name=profile_name,
             platform_id=platform_id,
+            signing_material=signing_material,
             signature_validity_period=signature_validity_period,
-            tags=tags,
         )
         self.signing_profiles[profile_name] = profile
+        self.tag_resource(profile.arn, tags)
         return profile
 
     def list_signing_platforms(self) -> List[Dict[str, Any]]:
@@ -194,6 +198,17 @@ class SignerBackend(BaseBackend):
         """
         return SignerBackend.platforms
 
+    def list_tags_for_resource(self, resource_arn: str) -> Dict[str, str]:
+        return self.tagger.get_tag_dict_for_resource(resource_arn)
+
+    def tag_resource(self, resource_arn: str, tags: Dict[str, str]) -> None:
+        self.tagger.tag_resource(
+            resource_arn, TaggingService.convert_dict_to_tags_input(tags)
+        )
+
+    def untag_resource(self, resource_arn: str, tag_keys: List[str]) -> None:
+        self.tagger.untag_resource_using_names(resource_arn, tag_keys)
+
 
 # Using the lambda-regions
 # boto3.Session().get_available_regions("signer") still returns an empty list
diff --git a/moto/signer/responses.py b/moto/signer/responses.py
index 86073dc48..143209290 100644
--- a/moto/signer/responses.py
+++ b/moto/signer/responses.py
@@ -1,11 +1,13 @@
 """Handles incoming signer requests, invokes methods, returns responses."""
 import json
+from typing import Any
+from urllib.parse import unquote
 
 from moto.core.responses import BaseResponse
 from .models import signer_backends, SignerBackend
 
 
-class signerResponse(BaseResponse):
+class SignerResponse(BaseResponse):
     def __init__(self) -> None:
         super().__init__(service_name="signer")
 
@@ -30,10 +32,12 @@ class signerResponse(BaseResponse):
         signature_validity_period = params.get("signatureValidityPeriod")
         platform_id = params.get("platformId")
         tags = params.get("tags")
+        signing_material = params.get("signingMaterial")
         profile = self.signer_backend.put_signing_profile(
             profile_name=profile_name,
             signature_validity_period=signature_validity_period,
             platform_id=platform_id,
+            signing_material=signing_material,
             tags=tags,
         )
         return json.dumps(profile.to_dict(full=False))
@@ -41,3 +45,30 @@ class signerResponse(BaseResponse):
     def list_signing_platforms(self) -> str:
         platforms = self.signer_backend.list_signing_platforms()
         return json.dumps(dict(platforms=platforms))
+
+    def list_tags_for_resource(self) -> str:
+        resource_arn = unquote(self.path.split("/tags/")[-1])
+        return json.dumps(
+            {"tags": self.signer_backend.list_tags_for_resource(resource_arn)}
+        )
+
+    def tag_resource(self) -> str:
+        resource_arn = unquote(self.path.split("/tags/")[-1])
+        tags = self._get_param("tags")
+        self.signer_backend.tag_resource(resource_arn, tags)
+        return "{}"
+
+    def untag_resource(self) -> str:
+        resource_arn = unquote(self.path.split("/tags/")[-1])
+        tag_keys = self.querystring.get("tagKeys")
+        self.signer_backend.untag_resource(resource_arn, tag_keys)  # type: ignore
+        return "{}"
+
+    def tags(self, request: Any, full_url: str, headers: Any) -> str:  # type: ignore[return]
+        self.setup_class(request, full_url, headers)
+        if request.method == "GET":
+            return self.list_tags_for_resource()
+        if request.method == "POST":
+            return self.tag_resource()
+        if request.method == "DELETE":
+            return self.untag_resource()
diff --git a/moto/signer/urls.py b/moto/signer/urls.py
index 4b67c84ed..a7cc666ee 100644
--- a/moto/signer/urls.py
+++ b/moto/signer/urls.py
@@ -1,15 +1,16 @@
 """signer base URL and path."""
-from .responses import signerResponse
+from .responses import SignerResponse
 
 url_bases = [
     r"https?://signer\.(.+)\.amazonaws\.com",
 ]
 
 
-response = signerResponse()
-
-
 url_paths = {
-    "{0}/signing-profiles/(?P<profile_name>[^/]+)$": response.dispatch,
-    "{0}/signing-platforms$": response.dispatch,
+    "{0}/tags/(?P<profile_arn>[^/]+)$": SignerResponse.dispatch,
+    "{0}/tags/(?P<arn_prefix>[^/]+)/signing-profiles/(?P<profile_name>[^/]+)$": SignerResponse.method_dispatch(
+        SignerResponse.tags  # type: ignore
+    ),
+    "{0}/signing-profiles/(?P<profile_name>[^/]+)$": SignerResponse.dispatch,
+    "{0}/signing-platforms$": SignerResponse.dispatch,
 }


Test patch:
diff --git a/tests/test_signer/test_signing_profiles.py b/tests/test_signer/test_signing_profiles.py
index 3e52b4337..049d359bf 100644
--- a/tests/test_signer/test_signing_profiles.py
+++ b/tests/test_signer/test_signing_profiles.py
@@ -20,9 +20,7 @@ def test_put_signing_profile():
 @mock_signer
 def test_get_signing_profile():
     client = boto3.client("signer", region_name="eu-west-1")
-    resp = client.put_signing_profile(
-        profileName="prof1", platformId="AWSLambda-SHA384-ECDSA"
-    )
+    client.put_signing_profile(profileName="prof1", platformId="AWSLambda-SHA384-ECDSA")
 
     resp = client.get_signing_profile(profileName="prof1")
 
@@ -38,25 +36,38 @@ def test_get_signing_profile():
 @mock_signer
 def test_get_signing_profile__with_args():
     client = boto3.client("signer", region_name="eu-west-1")
-    resp = client.put_signing_profile(
+    profile_arn = client.put_signing_profile(
         profileName="prof1",
         platformId="AWSLambda-SHA384-ECDSA",
         signatureValidityPeriod={"type": "DAYS", "value": 10},
+        signingMaterial={"certificateArn": "some arn"},
         tags={"k1": "v1", "k2": "v2"},
-    )
+    )["arn"]
 
     resp = client.get_signing_profile(profileName="prof1")
 
     assert resp["signatureValidityPeriod"] == {"type": "DAYS", "value": 10}
     assert resp["tags"] == {"k1": "v1", "k2": "v2"}
+    assert resp["signingMaterial"] == {"certificateArn": "some arn"}
+
+    tag_list = client.list_tags_for_resource(resourceArn=profile_arn)["tags"]
+    assert tag_list == {"k1": "v1", "k2": "v2"}
+
+    client.tag_resource(resourceArn=profile_arn, tags={"k3": "v3"})
+
+    tag_list = client.list_tags_for_resource(resourceArn=profile_arn)["tags"]
+    assert tag_list == {"k1": "v1", "k2": "v2", "k3": "v3"}
+
+    client.untag_resource(resourceArn=profile_arn, tagKeys=["k2"])
+
+    tag_list = client.list_tags_for_resource(resourceArn=profile_arn)["tags"]
+    assert tag_list == {"k1": "v1", "k3": "v3"}
 
 
 @mock_signer
 def test_cancel_signing_profile():
     client = boto3.client("signer", region_name="eu-west-1")
-    resp = client.put_signing_profile(
-        profileName="prof1", platformId="AWSLambda-SHA384-ECDSA"
-    )
+    client.put_signing_profile(profileName="prof1", platformId="AWSLambda-SHA384-ECDSA")
 
     client.cancel_signing_profile(profileName="prof1")