diff --git a/smithy-aws-cloudformation/src/main/java/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/CfnConverter.java b/smithy-aws-cloudformation/src/main/java/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/CfnConverter.java index bf99887250e..d04fe934c05 100644 --- a/smithy-aws-cloudformation/src/main/java/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/CfnConverter.java +++ b/smithy-aws-cloudformation/src/main/java/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/CfnConverter.java @@ -25,16 +25,14 @@ import java.util.Set; import software.amazon.smithy.aws.cloudformation.schema.CfnConfig; import software.amazon.smithy.aws.cloudformation.schema.CfnException; +import software.amazon.smithy.aws.cloudformation.schema.fromsmithy.mappers.TaggingMapper; import software.amazon.smithy.aws.cloudformation.schema.model.Property; import software.amazon.smithy.aws.cloudformation.schema.model.ResourceSchema; -import software.amazon.smithy.aws.cloudformation.schema.model.Tagging; import software.amazon.smithy.aws.cloudformation.traits.CfnNameTrait; import software.amazon.smithy.aws.cloudformation.traits.CfnResource; import software.amazon.smithy.aws.cloudformation.traits.CfnResourceIndex; import software.amazon.smithy.aws.cloudformation.traits.CfnResourceTrait; import software.amazon.smithy.aws.traits.ServiceTrait; -import software.amazon.smithy.aws.traits.tagging.AwsTagIndex; -import software.amazon.smithy.aws.traits.tagging.TaggableTrait; import software.amazon.smithy.jsonschema.JsonSchemaConverter; import software.amazon.smithy.jsonschema.JsonSchemaMapper; import software.amazon.smithy.jsonschema.PropertyNamingStrategy; @@ -56,7 +54,6 @@ import software.amazon.smithy.utils.StringUtils; public final class CfnConverter { - private static final String DEFAULT_TAGS_NAME = "Tags"; private ClassLoader classLoader = CfnConverter.class.getClassLoader(); private CfnConfig config = new CfnConfig(); private final List extensions = new ArrayList<>(); @@ -305,20 +302,6 @@ private ResourceSchema convertResource(ConversionEnvironment environment, Resour builder.addDefinition(definitionName, definition.getValue()); } - if (resourceShape.hasTrait(TaggableTrait.class)) { - AwsTagIndex tagsIndex = AwsTagIndex.of(environment.context.getModel()); - TaggableTrait trait = resourceShape.expectTrait(TaggableTrait.class); - Tagging.Builder tagBuilder = Tagging.builder() - .taggable(true) - .tagOnCreate(tagsIndex.isResourceTagOnCreate(resourceShape.getId())) - .tagProperty("/properties/" + getTagMemberName(resourceShape)) - .cloudFormationSystemTags(!trait.getDisableSystemTags()) - // Unless tag-on-create is supported, Smithy tagging means - .tagUpdatable(true); - - builder.tagging(tagBuilder.build()); - } - // Apply all the mappers' after methods. ResourceSchema resourceSchema = builder.build(); for (CfnMapper mapper : environment.mappers) { @@ -391,47 +374,8 @@ private StructureShape getCfnResourceStructure(Model model, ResourceShape resour } }); - injectTagsIfNecessary(builder, model, resource, cfnResource); + TaggingMapper.injectTagsMember(config, model, resource, builder); return builder.build(); } - - private String getTagMemberName(ResourceShape resource) { - return resource.getTrait(TaggableTrait.class) - .flatMap(TaggableTrait::getProperty) - .map(property -> { - if (config.getDisableCapitalizedProperties()) { - return property; - } - return StringUtils.capitalize(property); - }) - .orElse(DEFAULT_TAGS_NAME); - } - - private void injectTagsIfNecessary( - StructureShape.Builder builder, - Model model, - ResourceShape resource, - CfnResource cfnResource - ) { - String tagMemberName = getTagMemberName(resource); - if (resource.hasTrait(TaggableTrait.class)) { - AwsTagIndex tagIndex = AwsTagIndex.of(model); - TaggableTrait trait = resource.expectTrait(TaggableTrait.class); - if (!trait.getProperty().isPresent() || !cfnResource.getProperties() - .containsKey(trait.getProperty().get())) { - if (trait.getProperty().isPresent()) { - ShapeId definition = resource.getProperties().get(trait.getProperty().get()); - builder.addMember(tagMemberName, definition); - } else { - // A valid TagResource operation certainly has a single tags input member. - AwsTagIndex awsTagIndex = AwsTagIndex.of(model); - Optional tagOperation = tagIndex.getTagResourceOperation(resource.getId()); - MemberShape member = awsTagIndex.getTagsMember(tagOperation.get()).get(); - member = member.toBuilder().id(builder.getId().withMember(tagMemberName)).build(); - builder.addMember(member); - } - } - } - } } diff --git a/smithy-aws-cloudformation/src/main/java/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/mappers/CoreExtension.java b/smithy-aws-cloudformation/src/main/java/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/mappers/CoreExtension.java index 4b75d220dbd..38cadd973c9 100644 --- a/smithy-aws-cloudformation/src/main/java/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/mappers/CoreExtension.java +++ b/smithy-aws-cloudformation/src/main/java/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/mappers/CoreExtension.java @@ -34,6 +34,7 @@ public List getCfnMappers() { new IdentifierMapper(), new JsonAddMapper(), new MutabilityMapper(), - new RequiredMapper()); + new RequiredMapper(), + new TaggingMapper()); } } diff --git a/smithy-aws-cloudformation/src/main/java/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/mappers/HandlerPermissionMapper.java b/smithy-aws-cloudformation/src/main/java/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/mappers/HandlerPermissionMapper.java index 9a449c98ae8..ba45cbbb706 100644 --- a/smithy-aws-cloudformation/src/main/java/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/mappers/HandlerPermissionMapper.java +++ b/smithy-aws-cloudformation/src/main/java/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/mappers/HandlerPermissionMapper.java @@ -40,7 +40,7 @@ * @see handlers Docs */ @SmithyInternalApi -public final class HandlerPermissionMapper implements CfnMapper { +final class HandlerPermissionMapper implements CfnMapper { @Override public void before(Context context, ResourceSchema.Builder resourceSchema) { if (context.getConfig().getDisableHandlerPermissionGeneration()) { @@ -97,7 +97,7 @@ public void before(Context context, ResourceSchema.Builder resourceSchema) { .permissions(permissions).build())); } - private Set getPermissionsEntriesForOperation(Model model, ServiceShape service, ShapeId operationId) { + static Set getPermissionsEntriesForOperation(Model model, ServiceShape service, ShapeId operationId) { OperationShape operation = model.expectShape(operationId, OperationShape.class); Set permissionsEntries = new TreeSet<>(); diff --git a/smithy-aws-cloudformation/src/main/java/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/mappers/TaggingMapper.java b/smithy-aws-cloudformation/src/main/java/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/mappers/TaggingMapper.java new file mode 100644 index 00000000000..3ffad4609ff --- /dev/null +++ b/smithy-aws-cloudformation/src/main/java/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/mappers/TaggingMapper.java @@ -0,0 +1,113 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.aws.cloudformation.schema.fromsmithy.mappers; + +import static software.amazon.smithy.aws.cloudformation.schema.fromsmithy.mappers.HandlerPermissionMapper.getPermissionsEntriesForOperation; + +import java.util.Optional; +import software.amazon.smithy.aws.cloudformation.schema.CfnConfig; +import software.amazon.smithy.aws.cloudformation.schema.fromsmithy.CfnMapper; +import software.amazon.smithy.aws.cloudformation.schema.fromsmithy.Context; +import software.amazon.smithy.aws.cloudformation.schema.model.ResourceSchema; +import software.amazon.smithy.aws.cloudformation.schema.model.Tagging; +import software.amazon.smithy.aws.cloudformation.traits.CfnResource; +import software.amazon.smithy.aws.cloudformation.traits.CfnResourceIndex; +import software.amazon.smithy.aws.traits.tagging.AwsTagIndex; +import software.amazon.smithy.aws.traits.tagging.TaggableTrait; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.shapes.MemberShape; +import software.amazon.smithy.model.shapes.ResourceShape; +import software.amazon.smithy.model.shapes.ServiceShape; +import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.model.shapes.StructureShape; +import software.amazon.smithy.utils.SmithyInternalApi; +import software.amazon.smithy.utils.StringUtils; + +/** + * Generates the resource's Tagging configuration based on the AwsTagIndex, including + * the tagging property and operations that interact with tags. + * + * @see permissions property definition + */ +@SmithyInternalApi +public final class TaggingMapper implements CfnMapper { + private static final String DEFAULT_TAGS_NAME = "Tags"; + + @SmithyInternalApi + public static void injectTagsMember( + CfnConfig config, + Model model, + ResourceShape resource, + StructureShape.Builder builder + ) { + String tagMemberName = getTagMemberName(config, resource); + if (resource.hasTrait(TaggableTrait.class)) { + AwsTagIndex tagIndex = AwsTagIndex.of(model); + TaggableTrait trait = resource.expectTrait(TaggableTrait.class); + CfnResourceIndex resourceIndex = CfnResourceIndex.of(model); + CfnResource cfnResource = resourceIndex.getResource(resource).get(); + + if (!trait.getProperty().isPresent() || !cfnResource.getProperties() + .containsKey(trait.getProperty().get())) { + if (trait.getProperty().isPresent()) { + ShapeId definition = resource.getProperties().get(trait.getProperty().get()); + builder.addMember(tagMemberName, definition); + } else { + // A valid TagResource operation certainly has a single tags input member. + Optional tagOperation = tagIndex.getTagResourceOperation(resource.getId()); + MemberShape member = tagIndex.getTagsMember(tagOperation.get()).get(); + member = member.toBuilder().id(builder.getId().withMember(tagMemberName)).build(); + builder.addMember(member); + } + } + } + } + + @Override + public ResourceSchema after(Context context, ResourceSchema resourceSchema) { + ResourceShape resourceShape = context.getResource(); + if (!resourceShape.hasTrait(TaggableTrait.class)) { + return resourceSchema; + } + + Model model = context.getModel(); + ServiceShape service = context.getService(); + AwsTagIndex tagsIndex = AwsTagIndex.of(model); + TaggableTrait trait = resourceShape.expectTrait(TaggableTrait.class); + Tagging.Builder tagBuilder = Tagging.builder() + .taggable(true) + .tagOnCreate(tagsIndex.isResourceTagOnCreate(resourceShape.getId())) + .tagProperty("/properties/" + getTagMemberName(context.getConfig(), resourceShape)) + .cloudFormationSystemTags(!trait.getDisableSystemTags()) + // Unless tag-on-create is supported, Smithy tagging means + .tagUpdatable(true); + + // Add the tagging permissions based on the defined tagging operations. + tagsIndex.getTagResourceOperation(resourceShape) + .map(operation -> getPermissionsEntriesForOperation(model, service, operation)) + .ifPresent(tagBuilder::addPermissions); + tagsIndex.getListTagsForResourceOperation(resourceShape) + .map(operation -> getPermissionsEntriesForOperation(model, service, operation)) + .ifPresent(tagBuilder::addPermissions); + tagsIndex.getUntagResourceOperation(resourceShape) + .map(operation -> getPermissionsEntriesForOperation(model, service, operation)) + .ifPresent(tagBuilder::addPermissions); + + return resourceSchema.toBuilder().tagging(tagBuilder.build()).build(); + } + + private static String getTagMemberName(CfnConfig config, ResourceShape resource) { + return resource.getTrait(TaggableTrait.class) + .flatMap(TaggableTrait::getProperty) + .map(property -> { + if (config.getDisableCapitalizedProperties()) { + return property; + } + return StringUtils.capitalize(property); + }) + .orElse(DEFAULT_TAGS_NAME); + } +} diff --git a/smithy-aws-cloudformation/src/main/java/software/amazon/smithy/aws/cloudformation/schema/model/Tagging.java b/smithy-aws-cloudformation/src/main/java/software/amazon/smithy/aws/cloudformation/schema/model/Tagging.java index cd429ca796e..4a3073044b8 100644 --- a/smithy-aws-cloudformation/src/main/java/software/amazon/smithy/aws/cloudformation/schema/model/Tagging.java +++ b/smithy-aws-cloudformation/src/main/java/software/amazon/smithy/aws/cloudformation/schema/model/Tagging.java @@ -15,6 +15,10 @@ package software.amazon.smithy.aws.cloudformation.schema.model; +import java.util.Collection; +import java.util.Set; +import java.util.TreeSet; +import software.amazon.smithy.utils.SetUtils; import software.amazon.smithy.utils.SmithyBuilder; import software.amazon.smithy.utils.ToSmithyBuilder; @@ -27,6 +31,7 @@ public final class Tagging implements ToSmithyBuilder { private final boolean tagUpdatable; private final String tagProperty; private final boolean cloudFormationSystemTags; + private final Set permissions; private Tagging(Builder builder) { taggable = builder.taggable; @@ -34,6 +39,7 @@ private Tagging(Builder builder) { tagUpdatable = builder.tagUpdatable; cloudFormationSystemTags = builder.cloudFormationSystemTags; tagProperty = builder.tagProperty; + this.permissions = SetUtils.orderedCopyOf(builder.permissions); } public static Builder builder() { @@ -85,6 +91,15 @@ public String getTagProperty() { return tagProperty; } + /** + * Returns the set of permissions required to interact with this resource's tags. + * + * @return the set of permissions. + */ + public Set getPermissions() { + return permissions; + } + @Override public Builder toBuilder() { return builder() @@ -92,7 +107,8 @@ public Builder toBuilder() { .tagOnCreate(tagOnCreate) .tagUpdatable(tagUpdatable) .cloudFormationSystemTags(cloudFormationSystemTags) - .tagProperty(tagProperty); + .tagProperty(tagProperty) + .permissions(permissions); } public static final class Builder implements SmithyBuilder { @@ -101,6 +117,7 @@ public static final class Builder implements SmithyBuilder { private boolean tagUpdatable; private boolean cloudFormationSystemTags; private String tagProperty; + private final Set permissions = new TreeSet<>(); @Override public Tagging build() { @@ -131,5 +148,28 @@ public Builder tagProperty(String tagProperty) { this.tagProperty = tagProperty; return this; } + + public Builder permissions(Collection permissions) { + this.permissions.clear(); + this.permissions.addAll(permissions); + return this; + } + + public Builder addPermissions(Collection permissions) { + for (String permission : permissions) { + addPermission(permission); + } + return this; + } + + public Builder addPermission(String permission) { + this.permissions.add(permission); + return this; + } + + public Builder clearPermissions() { + this.permissions.clear(); + return this; + } } } diff --git a/smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/provider.definition.schema.v1.json b/smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/provider.definition.schema.v1.json index 5a4d0ac4565..6130be671a0 100644 --- a/smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/provider.definition.schema.v1.json +++ b/smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/provider.definition.schema.v1.json @@ -145,6 +145,13 @@ "description": "A reference to the Tags property in the schema.", "$ref": "http://json-schema.org/draft-07/schema#/properties/$ref", "default": "/properties/Tags" + }, + "permissions": { + "type": "array", + "items": { + "type": "string" + }, + "additionalItems": false } }, "required": [ diff --git a/smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/weather-service-wide.cfn.json b/smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/weather-service-wide.cfn.json index 7a9311de52f..77455953b7d 100644 --- a/smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/weather-service-wide.cfn.json +++ b/smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/weather-service-wide.cfn.json @@ -83,7 +83,12 @@ "tagProperty": "/properties/Tags", "tagUpdatable": true, "cloudFormationSystemTags": true, - "taggable": true + "taggable": true, + "permissions": [ + "weather:ListTagsForResource", + "weather:TagResource", + "weather:UntagResource" + ] }, "additionalProperties": false } diff --git a/smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/weather.cfn.json b/smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/weather.cfn.json index 7dd4df0905d..782a98e8ab5 100644 --- a/smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/weather.cfn.json +++ b/smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/weather.cfn.json @@ -82,7 +82,12 @@ "tagProperty": "/properties/Tags", "tagUpdatable": true, "cloudFormationSystemTags": true, - "taggable": true + "taggable": true, + "permissions": [ + "weather:ListTagsForCity", + "weather:TagCity", + "weather:UntagCity" + ] }, "additionalProperties": false }