Repository: smithy-lang/smithy
Pull request #2019 (2023-10-25 22:04:34)

Enable `aws.iam#disableConditionKeyInference` trait for service shape
*Description of changes:*
Enable `aws.iam#disableConditionKeyInference` trait for service shape


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/docs/source-2.0/aws/aws-iam.rst b/docs/source-2.0/aws/aws-iam.rst
index 06b70d2f1..5ba48d570 100644
--- a/docs/source-2.0/aws/aws-iam.rst
+++ b/docs/source-2.0/aws/aws-iam.rst
@@ -221,10 +221,47 @@ Condition keys in IAM policies can be evaluated with `condition operators`_.
 Summary
     Declares that the condition keys of a resource should not be inferred.
 Trait selector
-    ``resource``
+    ``:test(service, resource)``
 Value type
     Annotation trait
 
+When a service is marked with the ``aws.iam#disableConditionKeyInference``
+trait, all the resources bound to the service will not have condition
+keys automatically inferred from its identifiers and the identifiers
+of its ancestors.
+
+The following example shows resources ``MyResource1`` and ``MyResource2``
+have had condition key inference disabled because they are bound to a
+service marked with ``aws.iam#disableConditionKeyInference`` trait.
+
+.. code-block:: smithy
+
+    $version: "2"
+
+    namespace smithy.example
+
+    use aws.api#service
+    use aws.iam#disableConditionKeyInference
+
+    @service(sdkId: "My Value", arnNamespace: "myservice")
+    @disableConditionKeyInference
+    service MyService {
+        version: "2017-02-11"
+        resources: [MyResource1, MyResource2]
+    }
+
+    resource MyResource1 {
+        identifiers: {
+            foo: String
+        }
+    }
+
+    resource MyResource2 {
+        identifiers: {
+            foo: String
+        }
+    }
+
 A resource marked with the ``aws.iam#disableConditionKeyInference`` trait will
 not have its condition keys automatically inferred from its identifiers and
 the identifiers of its ancestors (if present.)
diff --git a/smithy-aws-iam-traits/src/main/java/software/amazon/smithy/aws/iam/traits/ConditionKeysIndex.java b/smithy-aws-iam-traits/src/main/java/software/amazon/smithy/aws/iam/traits/ConditionKeysIndex.java
index 5648ba664..af5c1ed4b 100644
--- a/smithy-aws-iam-traits/src/main/java/software/amazon/smithy/aws/iam/traits/ConditionKeysIndex.java
+++ b/smithy-aws-iam-traits/src/main/java/software/amazon/smithy/aws/iam/traits/ConditionKeysIndex.java
@@ -64,14 +64,14 @@ public final class ConditionKeysIndex implements KnowledgeIndex {
                 service.getResources().stream()
                         .flatMap(id -> OptionalUtils.stream(model.getShape(id)))
                         .forEach(resource -> {
-                            compute(model, service.getId(), arnRoot, resource, null);
+                            compute(model, service, arnRoot, resource, null);
                         });
 
                 // Compute the keys of operations of the service.
                 service.getOperations().stream()
                         .flatMap(id -> OptionalUtils.stream(model.getShape(id)))
                         .forEach(operation -> {
-                            compute(model, service.getId(), arnRoot, operation, null);
+                            compute(model, service, arnRoot, operation, null);
                         });
             });
         });
@@ -153,7 +153,7 @@ public final class ConditionKeysIndex implements KnowledgeIndex {
 
     private void compute(
             Model model,
-            ShapeId service,
+            ServiceShape service,
             String arnRoot,
             Shape subject,
             ResourceShape parent
@@ -163,21 +163,24 @@ public final class ConditionKeysIndex implements KnowledgeIndex {
 
     private void compute(
             Model model,
-            ShapeId service,
+            ServiceShape service,
             String arnRoot,
             Shape subject,
             ResourceShape parent,
             Set<String> parentDefinitions
     ) {
         Set<String> definitions = new HashSet<>(parentDefinitions);
-        resourceConditionKeys.get(service).put(subject.getId(), definitions);
+        resourceConditionKeys.get(service.getId()).put(subject.getId(), definitions);
         subject.getTrait(ConditionKeysTrait.class).ifPresent(trait -> definitions.addAll(trait.getValues()));
 
         // Continue recursing into resources and computing keys.
         subject.asResourceShape().ifPresent(resource -> {
+            boolean disableConditionKeyInference = resource.hasTrait(DisableConditionKeyInferenceTrait.class)
+                        || service.hasTrait(DisableConditionKeyInferenceTrait.class);
+
             // Add any inferred resource identifiers to the resource and to the service-wide definitions.
-            Map<String, String> childIdentifiers = !resource.hasTrait(DisableConditionKeyInferenceTrait.class)
-                    ? inferChildResourceIdentifiers(model, service, arnRoot, resource, parent)
+            Map<String, String> childIdentifiers = !disableConditionKeyInference
+                    ? inferChildResourceIdentifiers(model, service.getId(), arnRoot, resource, parent)
                     : MapUtils.of();
 
             // Compute the keys of each child operation, passing no keys.
diff --git a/smithy-aws-iam-traits/src/main/resources/META-INF/smithy/aws.iam.json b/smithy-aws-iam-traits/src/main/resources/META-INF/smithy/aws.iam.json
index 67e616c2b..37e57e633 100644
--- a/smithy-aws-iam-traits/src/main/resources/META-INF/smithy/aws.iam.json
+++ b/smithy-aws-iam-traits/src/main/resources/META-INF/smithy/aws.iam.json
@@ -41,9 +41,9 @@
             "type": "structure",
             "traits": {
                 "smithy.api#trait": {
-                    "selector": "resource"
+                    "selector": ":test(service, resource)"
                 },
-                "smithy.api#documentation": "Disables the automatic inference of condition keys of a resource."
+                "smithy.api#documentation": "Disables the automatic inference of condition keys of service's resources or a specific resource."
             }
         },
         "aws.iam#requiredActions": {


Test patch:
diff --git a/smithy-aws-iam-traits/src/test/java/software/amazon/smithy/aws/iam/traits/ConditionKeysIndexTest.java b/smithy-aws-iam-traits/src/test/java/software/amazon/smithy/aws/iam/traits/ConditionKeysIndexTest.java
index a2047f153..9b1d6cfb6 100644
--- a/smithy-aws-iam-traits/src/test/java/software/amazon/smithy/aws/iam/traits/ConditionKeysIndexTest.java
+++ b/smithy-aws-iam-traits/src/test/java/software/amazon/smithy/aws/iam/traits/ConditionKeysIndexTest.java
@@ -22,16 +22,11 @@ import static org.hamcrest.Matchers.empty;
 import static org.hamcrest.Matchers.is;
 import static org.hamcrest.Matchers.not;
 import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertTrue;
 
 import java.util.Optional;
-import java.util.stream.Collectors;
 import org.junit.jupiter.api.Test;
 import software.amazon.smithy.model.Model;
 import software.amazon.smithy.model.shapes.ShapeId;
-import software.amazon.smithy.model.validation.Severity;
-import software.amazon.smithy.model.validation.ValidatedResult;
-import software.amazon.smithy.model.validation.ValidationEvent;
 
 public class ConditionKeysIndexTest {
     @Test
@@ -65,4 +60,67 @@ public class ConditionKeysIndexTest {
         assertThat(index.getDefinedConditionKeys(service, ShapeId.from("smithy.example#GetResource2")).keySet(),
                 is(empty()));
     }
+
+    @Test
+    public void disableConditionKeyInferenceForResources() {
+        Model model = Model.assembler()
+                .addImport(getClass().getResource("disable-condition-key-inference-for-resources.smithy"))
+                .discoverModels(getClass().getClassLoader())
+                .assemble()
+                .unwrap();
+
+        ShapeId service = ShapeId.from("smithy.example#MyService");
+
+        ConditionKeysIndex index = ConditionKeysIndex.of(model);
+
+        assertThat(index.getConditionKeyNames(service),
+                containsInAnyOrder("my:service", "aws:operation1", "resource:1", "myservice:Resource1Id1"));
+
+        // Verify inference key myservice:Resource2Id2 does not exist
+        assertThat(index.getConditionKeyNames(service), not(contains("myservice:Resource2Id2")));
+
+        assertThat(index.getConditionKeyNames(service, ShapeId.from("smithy.example#Resource1")),
+                containsInAnyOrder("resource:1", "myservice:Resource1Id1"));
+
+        assertThat(index.getConditionKeyNames(service, ShapeId.from("smithy.example#Resource1")),
+                not(contains("myservice:Resource2Id2")));
+
+        assertThat(index.getConditionKeyNames(service, ShapeId.from("smithy.example#Resource2")),
+                containsInAnyOrder("resource:1", "myservice:Resource1Id1"));
+
+        assertThat(index.getConditionKeyNames(service, ShapeId.from("smithy.example#Resource2")),
+                not(contains("myservice:Resource2Id2")));
+    }
+
+    @Test
+    public void disableConditionKeyInferenceForService() {
+        Model model = Model.assembler()
+                .addImport(getClass().getResource("disable-condition-key-inference-for-service.smithy"))
+                .discoverModels(getClass().getClassLoader())
+                .assemble()
+                .unwrap();
+
+        ShapeId service = ShapeId.from("smithy.example#MyService");
+
+        ConditionKeysIndex index = ConditionKeysIndex.of(model);
+
+        assertThat(index.getConditionKeyNames(service),
+                containsInAnyOrder("my:service", "aws:operation1", "resource:1"));
+
+        // Verify inference key myservice:Resource1Id1 AND myservice:Resource2Id2 do not exist
+        assertThat(index.getConditionKeyNames(service),
+                not(contains("myservice:Resource1Id1", "myservice:Resource2Id2")));
+
+        assertThat(index.getConditionKeyNames(service, ShapeId.from("smithy.example#Resource1")),
+                not(contains("myservice:Resource1Id1", "myservice:Resource2Id2")));
+
+        assertThat(index.getConditionKeyNames(service, ShapeId.from("smithy.example#Resource1")),
+                contains("resource:1"));
+
+        assertThat(index.getConditionKeyNames(service, ShapeId.from("smithy.example#Resource2")),
+                contains("resource:1"));
+
+        assertThat(index.getConditionKeyNames(service, ShapeId.from("smithy.example#Resource2")),
+                not(contains("myservice:Resource1Id1", "myservice:Resource2Id2")));
+    }
 }
diff --git a/smithy-aws-iam-traits/src/test/resources/software/amazon/smithy/aws/iam/traits/disable-condition-key-inference-for-resources.smithy b/smithy-aws-iam-traits/src/test/resources/software/amazon/smithy/aws/iam/traits/disable-condition-key-inference-for-resources.smithy
new file mode 100644
index 000000000..d6d8ee926
--- /dev/null
+++ b/smithy-aws-iam-traits/src/test/resources/software/amazon/smithy/aws/iam/traits/disable-condition-key-inference-for-resources.smithy
@@ -0,0 +1,63 @@
+$version: "1.0"
+namespace smithy.example
+
+@aws.api#service(sdkId: "My")
+@aws.iam#defineConditionKeys("my:service": {type: "String", documentation: "Foo baz"})
+service MyService {
+  version: "2019-02-20",
+  operations: [Operation1],
+  resources: [Resource1]
+}
+
+@aws.iam#conditionKeys(["aws:operation1", "my:service"])
+operation Operation1 {}
+
+@aws.iam#conditionKeys(["resource:1"])
+resource Resource1 {
+  identifiers: {
+    id1: ArnString,
+  },
+  resources: [Resource2]
+}
+
+@aws.iam#disableConditionKeyInference
+resource Resource2 {
+  identifiers: {
+    id1: ArnString,
+    id2: FooString,
+  },
+  read: GetResource2,
+  list: ListResource2,
+}
+
+@readonly
+operation GetResource2 {
+    input: GetResource2Input
+}
+
+structure GetResource2Input {
+  @required
+  id1: ArnString,
+
+  @required
+  id2: FooString
+}
+
+@documentation("This is Foo")
+string FooString
+
+@readonly
+operation ListResource2 {
+    input: ListResource2Input,
+    output: ListResource2Output
+}
+
+structure ListResource2Input {
+  @required
+  id1: ArnString,
+}
+
+structure ListResource2Output {}
+
+@aws.api#arnReference(type: "ec2:Instance")
+string ArnString
diff --git a/smithy-aws-iam-traits/src/test/resources/software/amazon/smithy/aws/iam/traits/disable-condition-key-inference-for-service.smithy b/smithy-aws-iam-traits/src/test/resources/software/amazon/smithy/aws/iam/traits/disable-condition-key-inference-for-service.smithy
new file mode 100644
index 000000000..2c7396243
--- /dev/null
+++ b/smithy-aws-iam-traits/src/test/resources/software/amazon/smithy/aws/iam/traits/disable-condition-key-inference-for-service.smithy
@@ -0,0 +1,63 @@
+$version: "1.0"
+namespace smithy.example
+
+@aws.api#service(sdkId: "My")
+@aws.iam#defineConditionKeys("my:service": {type: "String", documentation: "Foo baz"})
+@aws.iam#disableConditionKeyInference
+service MyService {
+  version: "2019-02-20",
+  operations: [Operation1],
+  resources: [Resource1]
+}
+
+@aws.iam#conditionKeys(["aws:operation1", "my:service"])
+operation Operation1 {}
+
+@aws.iam#conditionKeys(["resource:1"])
+resource Resource1 {
+  identifiers: {
+    id1: ArnString,
+  },
+  resources: [Resource2]
+}
+
+resource Resource2 {
+  identifiers: {
+    id1: ArnString,
+    id2: FooString,
+  },
+  read: GetResource2,
+  list: ListResource2,
+}
+
+@readonly
+operation GetResource2 {
+  input: GetResource2Input
+}
+
+structure GetResource2Input {
+  @required
+  id1: ArnString,
+
+  @required
+  id2: FooString
+}
+
+@documentation("This is Foo")
+string FooString
+
+@readonly
+operation ListResource2 {
+  input: ListResource2Input,
+  output: ListResource2Output
+}
+
+structure ListResource2Input {
+  @required
+  id1: ArnString,
+}
+
+structure ListResource2Output {}
+
+@aws.api#arnReference(type: "ec2:Instance")
+string ArnString