Repository: smithy-lang/smithy
Pull request #2293 (2024-05-20 21:20:34)

Validator for duplicate names in iamResource trait
#### Background
[IamResourceTraitValidator](https://github.com/smithy-lang/smithy/blob/main/smithy-aws-iam-traits/src/main/java/software/amazon/smithy/aws/iam/traits/IamResourceTraitValidator.java) does not validates conflicting resource names in IAM space. For example, assume there exists two resource shapes Resource1 and Resource2. If I add trait @iamResource(name: "Resource1") on Resource2, there are conflicting resource names in the IAM space. This behavior is not detected/prevented by any validator in smithy.

#### Testing
Wrote a unit test for the validator.

---
By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.



Patch:
diff --git a/smithy-aws-iam-traits/src/main/java/software/amazon/smithy/aws/iam/traits/IamResourceTraitConflictingNameValidator.java b/smithy-aws-iam-traits/src/main/java/software/amazon/smithy/aws/iam/traits/IamResourceTraitConflictingNameValidator.java
new file mode 100644
index 000000000..9ea4fa3d9
--- /dev/null
+++ b/smithy-aws-iam-traits/src/main/java/software/amazon/smithy/aws/iam/traits/IamResourceTraitConflictingNameValidator.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package software.amazon.smithy.aws.iam.traits;
+
+import static software.amazon.smithy.model.validation.ValidationUtils.tickedList;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import software.amazon.smithy.model.Model;
+import software.amazon.smithy.model.knowledge.TopDownIndex;
+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.validation.AbstractValidator;
+import software.amazon.smithy.model.validation.ValidationEvent;
+import software.amazon.smithy.utils.SmithyInternalApi;
+
+/**
+ * Ensures that there is no resource name conflict in a service closure
+ * in IAM space after processing {@link IamResourceTrait}.
+ */
+@SmithyInternalApi
+public class IamResourceTraitConflictingNameValidator extends AbstractValidator {
+    @Override
+    public List<ValidationEvent> validate(Model model) {
+        TopDownIndex topDownIndex = TopDownIndex.of(model);
+        return model.shapes(ServiceShape.class)
+                .flatMap(shape -> validateService(topDownIndex, shape).stream())
+                .collect(Collectors.toList());
+    }
+
+    private List<ValidationEvent> validateService(TopDownIndex topDownIndex, ServiceShape service) {
+        List<ValidationEvent> events = new ArrayList<>();
+        Map<String, List<ShapeId>> resourceMap = new HashMap<>();
+        for (ResourceShape resource : topDownIndex.getContainedResources(service)) {
+            String resourceName = IamResourceTrait.resolveResourceName(resource);
+            resourceMap.computeIfAbsent(resourceName, k -> new ArrayList<>()).add(resource.getId());
+        }
+        resourceMap.entrySet().stream()
+                .filter(entry -> entry.getValue().size() > 1)
+                .map(entry -> error(service, String.format(
+                        "Multiple IAM resources defined with the same IAM resource name is not allowed in a service "
+                                + "closure, but found multiple resources named `%s` in the service `%s`: [%s]",
+                        entry.getKey(), service.getId(), tickedList(entry.getValue()))))
+                .forEach(events::add);
+        return events;
+    }
+}
diff --git a/smithy-aws-iam-traits/src/main/resources/META-INF/services/software.amazon.smithy.model.validation.Validator b/smithy-aws-iam-traits/src/main/resources/META-INF/services/software.amazon.smithy.model.validation.Validator
index 856e5dbe6..120767a57 100644
--- a/smithy-aws-iam-traits/src/main/resources/META-INF/services/software.amazon.smithy.model.validation.Validator
+++ b/smithy-aws-iam-traits/src/main/resources/META-INF/services/software.amazon.smithy.model.validation.Validator
@@ -1,3 +1,4 @@
 software.amazon.smithy.aws.iam.traits.ConditionKeysValidator
 software.amazon.smithy.aws.iam.traits.IamActionValidator
 software.amazon.smithy.aws.iam.traits.IamResourceTraitValidator
+software.amazon.smithy.aws.iam.traits.IamResourceTraitConflictingNameValidator


Test patch:
diff --git a/smithy-aws-iam-traits/src/test/resources/software/amazon/smithy/aws/iam/traits/errorfiles/iam-resources/duplicate-iam-resources-name.errors b/smithy-aws-iam-traits/src/test/resources/software/amazon/smithy/aws/iam/traits/errorfiles/iam-resources/duplicate-iam-resources-name.errors
new file mode 100644
index 000000000..976e0c872
--- /dev/null
+++ b/smithy-aws-iam-traits/src/test/resources/software/amazon/smithy/aws/iam/traits/errorfiles/iam-resources/duplicate-iam-resources-name.errors
@@ -0,0 +1,1 @@
+[ERROR] smithy.example#MyService: Multiple IAM resources defined with the same IAM resource name is not allowed in a service closure, but found multiple resources named `Beer` in the service `smithy.example#MyService`: [`smithy.example#BadIamResourceName1`, `smithy.example#BadIamResourceName2`, `smithy.example#Beer`] | IamResourceTraitConflictingName
diff --git a/smithy-aws-iam-traits/src/test/resources/software/amazon/smithy/aws/iam/traits/errorfiles/iam-resources/duplicate-iam-resources-name.smithy b/smithy-aws-iam-traits/src/test/resources/software/amazon/smithy/aws/iam/traits/errorfiles/iam-resources/duplicate-iam-resources-name.smithy
new file mode 100644
index 000000000..9833e42db
--- /dev/null
+++ b/smithy-aws-iam-traits/src/test/resources/software/amazon/smithy/aws/iam/traits/errorfiles/iam-resources/duplicate-iam-resources-name.smithy
@@ -0,0 +1,39 @@
+$version: "2"
+namespace smithy.example
+
+use aws.api#arn
+
+@aws.api#service(sdkId: "My")
+@aws.iam#defineConditionKeys("foo:baz": {type: "String", documentation: "Foo baz"})
+service MyService {
+    version: "2019-02-20",
+    resources: [
+        BadIamResourceName1,
+        BadIamResourceName2,
+        Beer,
+        ShouldNotThrowAnErrorFirst,
+        ShouldNotThrowAnErrorSecond
+    ]
+}
+
+resource Beer {}
+
+@aws.iam#iamResource(name: "Beer")
+resource BadIamResourceName1 {}
+
+@aws.iam#iamResource(name: "Beer")
+resource BadIamResourceName2 {}
+
+@aws.iam#iamResource(name: "shouldNotThrowErrorSecond")
+resource ShouldNotThrowAnErrorFirst {
+    identifiers: {
+        beerId: String
+    }
+}
+
+@aws.iam#iamResource(name: "shouldNotThrowErrorFirst")
+resource ShouldNotThrowAnErrorSecond {
+    identifiers: {
+        arn: String
+    }
+}