Repository: getmoto/moto

Issue:
Enforce tagging restrictions?
There are a number of restrictions governing the content of AWS tags: https://docs.aws.amazon.com/awsaccountbilling/latest/aboutv2/allocation-tag-restrictions.html

It would be useful if moto enforced these - it doesn't appear to for S3 object tags at least.


Discussion:
Good idea; setting as a feature request.
Looks like this no one's picked this up. I can make a contribution for this as part of Hacktoberfest. 
Follow-up, these restrictions would be applicable to all AWS resources, not just S3.
That would be very welcome @shreeshacoder. 
To give some pointers: There is a `moto.utilities.TaggingService` used to store tags, but it's only in use for a few services. The class already contains some validation-options - I don't know whether all services could reuse these validation rules though.
You're right.

[AWS calls out](https://docs.aws.amazon.com/general/latest/gr/aws_tagging.html) that tagging restrictions are service specific, but there's ones that are applicable to all services:

- tags prefixed with `aws:` are reserved for AWS's use
- The tag key must be a minimum of 1 and a maximum of 128 Unicode characters in UTF-8.
- The tag value must be a minimum of 0 and a maximum of 256 Unicode characters in UTF-8.
- In general, allowed characters in tags are letters, numbers, spaces representable in UTF-8, and the following characters: _ . : / = + - @ .

Think adding restrictions for the above would be a good start, and would also fix the call out in this issue.
The `TaggingService` has these checks already: https://github.com/spulec/moto/blob/230e34748fab26b58b5ce4311fa5b2f93f6f1da1/moto/utilities/tagging_service.py#L104-L158

In the case of S3, it's uses the `TaggingService`, but never invokes the `validate_tags()` method. 

Resolving patch:
diff --git a/moto/s3/exceptions.py b/moto/s3/exceptions.py
--- a/moto/s3/exceptions.py
+++ b/moto/s3/exceptions.py
@@ -73,7 +73,7 @@ def __init__(self, *args, **kwargs):
                 "select a different name and try again"
             ),
             *args,
-            **kwargs
+            **kwargs,
         )
 
 
@@ -139,7 +139,7 @@ def __init__(self, *args, **kwargs):
                 "list must be specified in order by part number."
             ),
             *args,
-            **kwargs
+            **kwargs,
         )
 
 
@@ -155,7 +155,7 @@ def __init__(self, *args, **kwargs):
                 "entity tag might not have matched the part's entity tag."
             ),
             *args,
-            **kwargs
+            **kwargs,
         )
 
 
@@ -167,7 +167,7 @@ def __init__(self, *args, **kwargs):
             "EntityTooSmall",
             "Your proposed upload is smaller than the minimum allowed object size.",
             *args,
-            **kwargs
+            **kwargs,
         )
 
 
@@ -181,7 +181,7 @@ def __init__(self, method, *args, **kwargs):
                 method
             ),
             *args,
-            **kwargs
+            **kwargs,
         )
 
 
@@ -193,7 +193,7 @@ def __init__(self, *args, **kwargs):
             "IllegalLocationConstraintException",
             "The unspecified location constraint is incompatible for the region specific endpoint this request was sent to.",
             *args,
-            **kwargs
+            **kwargs,
         )
 
 
@@ -205,7 +205,7 @@ def __init__(self, *args, **kwargs):
             "MalformedXML",
             "The XML you provided was not well-formed or did not validate against our published schema",
             *args,
-            **kwargs
+            **kwargs,
         )
 
 
@@ -217,7 +217,7 @@ def __init__(self, *args, **kwargs):
             "MalformedACLError",
             "The XML you provided was not well-formed or did not validate against our published schema",
             *args,
-            **kwargs
+            **kwargs,
         )
 
 
@@ -266,7 +266,7 @@ def __init__(self, *args, **kwargs):
             "InvalidArgument",
             "The notification destination service region is not valid for the bucket location constraint",
             *args,
-            **kwargs
+            **kwargs,
         )
 
 
@@ -278,7 +278,7 @@ def __init__(self, *args, **kwargs):
             "InvalidArgument",
             "The event is not supported for notifications",
             *args,
-            **kwargs
+            **kwargs,
         )
 
 
@@ -290,7 +290,7 @@ def __init__(self, *args, **kwargs):
             "InvalidStorageClass",
             "The storage class you specified is not valid",
             *args,
-            **kwargs
+            **kwargs,
         )
 
 
@@ -311,7 +311,7 @@ def __init__(self, *args, **kwargs):
             "InvalidTag",
             "Cannot provide multiple Tags with the same key",
             *args,
-            **kwargs
+            **kwargs,
         )
 
 
@@ -341,7 +341,7 @@ def __init__(self, *args, **kwargs):
             "InvalidToken",
             "The provided token is malformed or otherwise invalid.",
             *args,
-            **kwargs
+            **kwargs,
         )
 
 
@@ -353,7 +353,7 @@ def __init__(self, *args, **kwargs):
             "InvalidToken",
             "The provided token is malformed or otherwise invalid.",
             *args,
-            **kwargs
+            **kwargs,
         )
 
 
@@ -365,7 +365,7 @@ def __init__(self, *args, **kwargs):
             "InvalidAccessKeyId",
             "The AWS Access Key Id you provided does not exist in our records.",
             *args,
-            **kwargs
+            **kwargs,
         )
 
 
@@ -377,7 +377,7 @@ def __init__(self, *args, **kwargs):
             "InvalidAccessKeyId",
             "The AWS Access Key Id you provided does not exist in our records.",
             *args,
-            **kwargs
+            **kwargs,
         )
 
 
@@ -389,7 +389,7 @@ def __init__(self, *args, **kwargs):
             "SignatureDoesNotMatch",
             "The request signature we calculated does not match the signature you provided. Check your key and signing method.",
             *args,
-            **kwargs
+            **kwargs,
         )
 
 
@@ -401,7 +401,7 @@ def __init__(self, *args, **kwargs):
             "SignatureDoesNotMatch",
             "The request signature we calculated does not match the signature you provided. Check your key and signing method.",
             *args,
-            **kwargs
+            **kwargs,
         )
 
 
@@ -413,7 +413,7 @@ def __init__(self, *args, **kwargs):
             "NoSuchPublicAccessBlockConfiguration",
             "The public access block configuration was not found",
             *args,
-            **kwargs
+            **kwargs,
         )
 
 
@@ -425,7 +425,7 @@ def __init__(self, *args, **kwargs):
             "InvalidRequest",
             "Must specify at least one configuration.",
             *args,
-            **kwargs
+            **kwargs,
         )
 
 
@@ -458,7 +458,7 @@ def __init__(self, upload_id, *args, **kwargs):
             "NoSuchUpload",
             "The specified upload does not exist. The upload ID may be invalid, or the upload may have been aborted or completed.",
             *args,
-            **kwargs
+            **kwargs,
         )
 
 
@@ -472,7 +472,7 @@ def __init__(self, failed_condition, **kwargs):
             "PreconditionFailed",
             "At least one of the pre-conditions you specified did not hold",
             condition=failed_condition,
-            **kwargs
+            **kwargs,
         )
 
 
@@ -487,7 +487,7 @@ def __init__(self, range_requested, actual_size, **kwargs):
             "The requested range is not satisfiable",
             range_requested=range_requested,
             actual_size=actual_size,
-            **kwargs
+            **kwargs,
         )
 
 
@@ -499,7 +499,7 @@ def __init__(self, *args, **kwargs):
             "InvalidArgument",
             "The continuation token provided is incorrect",
             *args,
-            **kwargs
+            **kwargs,
         )
 
 
@@ -556,5 +556,14 @@ def __init__(self, value, *args, **kwargs):
             "FilterRule.Name",
             value,
             *args,
-            **kwargs
+            **kwargs,
+        )
+
+
+class InvalidTagError(S3ClientError):
+    code = 400
+
+    def __init__(self, value, *args, **kwargs):
+        super(InvalidTagError, self).__init__(
+            "InvalidTag", value, *args, **kwargs,
         )
diff --git a/moto/s3/models.py b/moto/s3/models.py
--- a/moto/s3/models.py
+++ b/moto/s3/models.py
@@ -22,7 +22,7 @@
 from moto.core.utils import iso_8601_datetime_without_milliseconds_s3, rfc_1123_datetime
 from moto.cloudwatch.models import MetricDatum
 from moto.utilities.tagging_service import TaggingService
-from .exceptions import (
+from moto.s3.exceptions import (
     AccessDeniedByLock,
     BucketAlreadyExists,
     BucketNeedsToBeNew,
@@ -41,6 +41,7 @@
     InvalidPublicAccessBlockConfiguration,
     WrongPublicAccessBlockAccountIdError,
     NoSuchUpload,
+    InvalidTagError,
 )
 from .cloud_formation import cfn_to_api_encryption, is_replacement_update
 from .utils import clean_key_name, _VersionedKeyStore, undo_clean_key_name
@@ -1616,9 +1617,13 @@ def get_object_tagging(self, key):
     def set_key_tags(self, key, tags, key_name=None):
         if key is None:
             raise MissingKey(key_name)
+        boto_tags_dict = self.tagger.convert_dict_to_tags_input(tags)
+        errmsg = self.tagger.validate_tags(boto_tags_dict)
+        if errmsg:
+            raise InvalidTagError(errmsg)
         self.tagger.delete_all_tags_for_resource(key.arn)
         self.tagger.tag_resource(
-            key.arn, [{"Key": k, "Value": v} for (k, v) in tags.items()],
+            key.arn, boto_tags_dict,
         )
         return key
 
diff --git a/moto/utilities/tagging_service.py b/moto/utilities/tagging_service.py
--- a/moto/utilities/tagging_service.py
+++ b/moto/utilities/tagging_service.py
@@ -156,3 +156,8 @@ def validate_tags(self, tags):
             if errors
             else ""
         )
+
+    @staticmethod
+    def convert_dict_to_tags_input(tags):
+        """ Given a dictionary, return generic boto params for tags """
+        return [{"Key": k, "Value": v} for (k, v) in tags.items()]


Test patch:
diff --git a/tests/test_s3/test_s3.py b/tests/test_s3/test_s3.py
--- a/tests/test_s3/test_s3.py
+++ b/tests/test_s3/test_s3.py
@@ -34,6 +34,7 @@
 
 from moto import settings, mock_s3, mock_s3_deprecated, mock_config
 import moto.s3.models as s3model
+from moto.s3.exceptions import InvalidTagError
 from moto.core.exceptions import InvalidNextTokenException
 from moto.settings import get_s3_default_key_buffer_size, S3_UPLOAD_PART_MIN_SIZE
 
@@ -3411,6 +3412,19 @@ def test_boto3_copy_object_with_replacement_tagging():
         Bucket="mybucket", Key="original", Body=b"test", Tagging="tag=old"
     )
 
+    # using system tags will fail
+    with pytest.raises(ClientError) as err:
+        client.copy_object(
+            CopySource={"Bucket": "mybucket", "Key": "original"},
+            Bucket="mybucket",
+            Key="copy1",
+            TaggingDirective="REPLACE",
+            Tagging="aws:tag=invalid_key",
+        )
+
+    e = err.value
+    e.response["Error"]["Code"].should.equal("InvalidTag")
+
     client.copy_object(
         CopySource={"Bucket": "mybucket", "Key": "original"},
         Bucket="mybucket",
@@ -3936,6 +3950,13 @@ def test_boto3_put_object_with_tagging():
     key = "key-with-tags"
     s3.create_bucket(Bucket=bucket_name)
 
+    # using system tags will fail
+    with pytest.raises(ClientError) as err:
+        s3.put_object(Bucket=bucket_name, Key=key, Body="test", Tagging="aws:foo=bar")
+
+    e = err.value
+    e.response["Error"]["Code"].should.equal("InvalidTag")
+
     s3.put_object(Bucket=bucket_name, Key=key, Body="test", Tagging="foo=bar")
 
     s3.get_object_tagging(Bucket=bucket_name, Key=key)["TagSet"].should.contain(
@@ -4665,6 +4686,17 @@ def test_boto3_put_object_tagging():
 
     s3.put_object(Bucket=bucket_name, Key=key, Body="test")
 
+    # using system tags will fail
+    with pytest.raises(ClientError) as err:
+        s3.put_object_tagging(
+            Bucket=bucket_name,
+            Key=key,
+            Tagging={"TagSet": [{"Key": "aws:item1", "Value": "foo"},]},
+        )
+
+    e = err.value
+    e.response["Error"]["Code"].should.equal("InvalidTag")
+
     resp = s3.put_object_tagging(
         Bucket=bucket_name,
         Key=key,