From 9a283e6c96bc715c65a73a035030834606f85a6d Mon Sep 17 00:00:00 2001 From: Kevin Stich Date: Thu, 7 Nov 2024 15:11:47 -0800 Subject: [PATCH] Inject tagging operation permissions for CFN This commit updates the CFN resource schema generation to fill in the permissions field of the tagging configuration for resources. The AwsTagIndex is used to find the APIs and their relevant required actions to invoke. This also refactors the Tagging updates into a single Mapper so the updates aren't strewn about the general CfnConverter. --- .../schema/fromsmithy/CfnConverter.java | 60 +--------- .../fromsmithy/mappers/CoreExtension.java | 3 +- .../mappers/HandlerPermissionMapper.java | 4 +- .../fromsmithy/mappers/TaggingMapper.java | 113 ++++++++++++++++++ .../cloudformation/schema/model/Tagging.java | 42 ++++++- .../provider.definition.schema.v1.json | 7 ++ .../fromsmithy/weather-service-wide.cfn.json | 7 +- .../schema/fromsmithy/weather.cfn.json | 7 +- 8 files changed, 179 insertions(+), 64 deletions(-) create mode 100644 smithy-aws-cloudformation/src/main/java/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/mappers/TaggingMapper.java 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 }