Repository: smithy-lang/smithy
Pull request #2036 (2023-11-08 23:20:49)

Add `disableConditionKeyInheritance` to `iamResource`
This commit adds a `disableConditionKeyInheritance` property to the `iamResource` trait, which can be set to `true` to decouple an IAM resource's condition keys from those of its parent resource(s). This can be combined with the `conditionKeys` trait to explicitly configure an IAM resource's condition keys.

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 a7e2ae28e..ee39923db 100644
--- a/docs/source-2.0/aws/aws-iam.rst
+++ b/docs/source-2.0/aws/aws-iam.rst
@@ -100,6 +100,12 @@ members:
       - ``string``
       - A relative URL path that defines more information about the resource
         within a set of IAM-related documentation.
+    * - disableConditionKeyInheritance
+      - ``boolean``
+      - When set to ``true``, decouples this IAM resource's condition keys from
+        those of its parent resource(s). This can be used in combination with
+        the :ref:`aws.iam#conditionKeys-trait` trait to isolate a resource's
+        condition keys from those of its parent(s).
 
 The following example defines a simple resource with a name in AWS IAM that
 deviates from the :ref:`shape name of the shape ID <shape-id>` of the resource.
@@ -751,7 +757,7 @@ Given the following model,
     resource MyResource {
         identifiers: {foo: String}
         operations: [MyOperation]
-        resources: [MyInnerResource]
+        resources: [MyInnerResource, MyDetachedResource, MyCustomResource]
     }
 
     @iamResource(name: "InnerResource")
@@ -759,6 +765,19 @@ Given the following model,
         identifiers: {yum: String}
     }
 
+    @disableConditionKeyInference
+    @iamResource(disableConditionKeyInheritance: true)
+    resource MyDetachedResource {
+        identifiers: {yum: String}
+    }
+
+    @disableConditionKeyInference
+    @iamResource(disableConditionKeyInheritance: true)
+    @conditionKeys(["aws:region"])
+    resource MyCustomResource {
+        identifiers: {yum: String}
+    }
+
     @conditionKeys(["aws:region"])
     operation MyOperation {}
 
@@ -779,6 +798,11 @@ The computed condition keys for the service are:
           * ``myservice:MyResourceFoo``
           * ``otherservice:Bar``
           * ``myservice:InnerResourceYum``
+    * - ``MyDetachedResource``
+      - None
+    * - ``MyCustomResource``
+      -
+          * ``aws:region``
     * - ``MyOperation``
       -
           * ``aws:region``
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 af5c1ed4b..346616d51 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
@@ -169,7 +169,12 @@ public final class ConditionKeysIndex implements KnowledgeIndex {
             ResourceShape parent,
             Set<String> parentDefinitions
     ) {
-        Set<String> definitions = new HashSet<>(parentDefinitions);
+        Set<String> definitions = new HashSet<>();
+        if (!subject.hasTrait(IamResourceTrait.ID)
+                || !subject.expectTrait(IamResourceTrait.class).isDisableConditionKeyInheritance()
+        ) {
+            definitions.addAll(parentDefinitions);
+        }
         resourceConditionKeys.get(service.getId()).put(subject.getId(), definitions);
         subject.getTrait(ConditionKeysTrait.class).ifPresent(trait -> definitions.addAll(trait.getValues()));
 
diff --git a/smithy-aws-iam-traits/src/main/java/software/amazon/smithy/aws/iam/traits/IamResourceTrait.java b/smithy-aws-iam-traits/src/main/java/software/amazon/smithy/aws/iam/traits/IamResourceTrait.java
index 2e3bed2fb..1787b320d 100644
--- a/smithy-aws-iam-traits/src/main/java/software/amazon/smithy/aws/iam/traits/IamResourceTrait.java
+++ b/smithy-aws-iam-traits/src/main/java/software/amazon/smithy/aws/iam/traits/IamResourceTrait.java
@@ -32,13 +32,15 @@ public final class IamResourceTrait extends AbstractTrait
         implements ToSmithyBuilder<IamResourceTrait> {
     public static final ShapeId ID = ShapeId.from("aws.iam#iamResource");
 
-    public final String name;
-    public final String relativeDocumentation;
+    private final String name;
+    private final String relativeDocumentation;
+    private final boolean disableConditionKeyInheritance;
 
     private IamResourceTrait(Builder builder) {
         super(ID, builder.getSourceLocation());
         name = builder.name;
         relativeDocumentation = builder.relativeDocumentation;
+        disableConditionKeyInheritance = builder.disableConditionKeyInheritance;
     }
 
     /**
@@ -47,7 +49,7 @@ public final class IamResourceTrait extends AbstractTrait
      * @return Returns the name.
      */
     public Optional<String> getName() {
-        return Optional.of(name);
+        return Optional.ofNullable(name);
     }
 
     /**
@@ -60,6 +62,16 @@ public final class IamResourceTrait extends AbstractTrait
         return Optional.ofNullable(relativeDocumentation);
     }
 
+    /**
+     * Gets if this IAM resource's condition keys are decoupled from
+     * those of its parent resource(s).
+     *
+     * @return Returns true if condition key inheritance is disabled.
+     */
+    public boolean isDisableConditionKeyInheritance() {
+        return disableConditionKeyInheritance;
+    }
+
     public static Builder builder() {
         return new Builder();
     }
@@ -93,6 +105,7 @@ public final class IamResourceTrait extends AbstractTrait
     public static final class Builder extends AbstractTraitBuilder<IamResourceTrait, Builder> {
         private String name;
         private String relativeDocumentation;
+        private boolean disableConditionKeyInheritance;
 
         private Builder() {}
 
@@ -110,5 +123,10 @@ public final class IamResourceTrait extends AbstractTrait
             this.relativeDocumentation = relativeDocumentation;
             return this;
         }
+
+        public Builder disableConditionKeyInheritance(boolean disableConditionKeyInheritance) {
+            this.disableConditionKeyInheritance = disableConditionKeyInheritance;
+            return this;
+        }
     }
 }
diff --git a/smithy-aws-iam-traits/src/main/resources/META-INF/smithy/aws.iam.smithy b/smithy-aws-iam-traits/src/main/resources/META-INF/smithy/aws.iam.smithy
index 47fd14293..157028602 100644
--- a/smithy-aws-iam-traits/src/main/resources/META-INF/smithy/aws.iam.smithy
+++ b/smithy-aws-iam-traits/src/main/resources/META-INF/smithy/aws.iam.smithy
@@ -25,7 +25,7 @@ list conditionKeys {
 string conditionKeyValue
 
 /// Defines the set of condition keys that appear within a service in addition to
-///  inferred and global condition keys.
+/// inferred and global condition keys.
 @trait(selector: "service")
 map defineConditionKeys {
     key: IamIdentifier
@@ -65,9 +65,13 @@ structure iamResource {
     /// The name of the resource in AWS IAM.
     name: String
 
-    ///  A relative URL path that defines more information about the resource
-    ///  within a set of IAM-related documentation.
+    /// A relative URL path that defines more information about the resource
+    /// within a set of IAM-related documentation.
     relativeDocumentation: String
+
+    /// When set to `true`, decouples this IAM resource's condition keys from
+    /// those of its parent resource(s).
+    disableConditionKeyInheritance: Boolean
 }
 
 /// Other actions that the invoker must be authorized to perform when executing the targeted operation.
@@ -112,8 +116,8 @@ structure ConditionKeyDefinition {
     /// A valid URL that defines more information about the condition key.
     externalDocumentation: String
 
-    ///  A relative URL path that defines more information about the condition key
-    ///  within a set of IAM-related documentation.
+    /// A relative URL path that defines more information about the condition key
+    /// within a set of IAM-related documentation.
     relativeDocumentation: String
 }
 


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 e47054780..d5e7f9c8b 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
@@ -49,6 +49,11 @@ public class ConditionKeysIndexTest {
         assertThat(index.getConditionKeyNames(service, ShapeId.from("smithy.example#Resource2")),
                 containsInAnyOrder("aws:accountId", "foo:baz",
                         "myservice:Resource1Id1", "myservice:ResourceTwoId2"));
+        // This resource has inheritance disabled.
+        assertThat(index.getConditionKeyNames(service, ShapeId.from("smithy.example#Resource3")), empty());
+        // This resource has inheritance disabled and an explicit list provided.
+        assertThat(index.getConditionKeyNames(service, ShapeId.from("smithy.example#Resource4")),
+                contains("foo:baz"));
         // Note that while this operation binds identifiers, it contains no unique ConditionKeys to bind.
         assertThat(index.getConditionKeyNames(service, ShapeId.from("smithy.example#GetResource2")), is(empty()));
 
diff --git a/smithy-aws-iam-traits/src/test/java/software/amazon/smithy/aws/iam/traits/IamResourceTraitTest.java b/smithy-aws-iam-traits/src/test/java/software/amazon/smithy/aws/iam/traits/IamResourceTraitTest.java
index eb7bc1837..dc03eb967 100644
--- a/smithy-aws-iam-traits/src/test/java/software/amazon/smithy/aws/iam/traits/IamResourceTraitTest.java
+++ b/smithy-aws-iam-traits/src/test/java/software/amazon/smithy/aws/iam/traits/IamResourceTraitTest.java
@@ -16,6 +16,7 @@
 package software.amazon.smithy.aws.iam.traits;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
 import org.junit.jupiter.api.Test;
@@ -37,5 +38,6 @@ public class IamResourceTraitTest {
         assertTrue(superResource.hasTrait(IamResourceTrait.class));
         assertEquals(superResource.expectTrait(IamResourceTrait.class).getName().get(), "super");
         assertEquals(superResource.expectTrait(IamResourceTrait.class).getRelativeDocumentation().get(), "API-Super.html");
+        assertFalse(superResource.expectTrait(IamResourceTrait.class).isDisableConditionKeyInheritance());
     }
 }
diff --git a/smithy-aws-iam-traits/src/test/resources/software/amazon/smithy/aws/iam/traits/iam-resource.smithy b/smithy-aws-iam-traits/src/test/resources/software/amazon/smithy/aws/iam/traits/iam-resource.smithy
index 55fc02b9a..627e72602 100644
--- a/smithy-aws-iam-traits/src/test/resources/software/amazon/smithy/aws/iam/traits/iam-resource.smithy
+++ b/smithy-aws-iam-traits/src/test/resources/software/amazon/smithy/aws/iam/traits/iam-resource.smithy
@@ -1,31 +1,34 @@
-$version: "1.0"
+$version: "2.0"
 
 namespace smithy.example
 
 use aws.api#arn
+use aws.api#service
+use aws.iam#iamResource
 
-@aws.api#service(sdkId: "My")
+@service(sdkId: "My")
 service MyService {
-    version: "2020-07-02",
+    version: "2020-07-02"
     resources: [SuperResource]
 }
 
-@aws.iam#iamResource(
-    name: "super",
+@iamResource(
+    name: "super"
     relativeDocumentation: "API-Super.html"
+    disableConditionKeyInheritance: false
 )
 @arn(template: "super/{id1}")
 resource SuperResource {
     identifiers: {
-        id1: String,
-    },
+        id1: String
+    }
     read: GetResource
 }
 
 @readonly
 operation GetResource {
-    input: GetResourceInput,
-    output: GetResourceOutput,
+    input: GetResourceInput
+    output: GetResourceOutput
 }
 
 structure GetResourceInput {
@@ -34,9 +37,9 @@ structure GetResourceInput {
 }
 
 structure GetResourceOutput {
-    super: Super,
+    super: Super
 }
 
 structure Super {
-    id1: String,
+    id1: String
 }
diff --git a/smithy-aws-iam-traits/src/test/resources/software/amazon/smithy/aws/iam/traits/successful-condition-keys.smithy b/smithy-aws-iam-traits/src/test/resources/software/amazon/smithy/aws/iam/traits/successful-condition-keys.smithy
index a7c0999c0..f6a209a05 100644
--- a/smithy-aws-iam-traits/src/test/resources/software/amazon/smithy/aws/iam/traits/successful-condition-keys.smithy
+++ b/smithy-aws-iam-traits/src/test/resources/software/amazon/smithy/aws/iam/traits/successful-condition-keys.smithy
@@ -1,8 +1,15 @@
 $version: "1.0"
 namespace smithy.example
 
-@aws.api#service(sdkId: "My")
-@aws.iam#defineConditionKeys(
+use aws.api#arnReference
+use aws.api#service
+use aws.iam#conditionKeys
+use aws.iam#defineConditionKeys
+use aws.iam#disableConditionKeyInference
+use aws.iam#iamResource
+
+@service(sdkId: "My")
+@defineConditionKeys(
   "foo:baz": {
     type: "String",
     documentation: "Foo baz",
@@ -15,18 +22,18 @@ service MyService {
   resources: [Resource1]
 }
 
-@aws.iam#conditionKeys(["aws:accountId", "foo:baz"])
+@conditionKeys(["aws:accountId", "foo:baz"])
 operation Operation1 {}
 
-@aws.iam#conditionKeys(["aws:accountId", "foo:baz"])
+@conditionKeys(["aws:accountId", "foo:baz"])
 resource Resource1 {
   identifiers: {
     id1: ArnString,
   },
-  resources: [Resource2]
+  resources: [Resource2, Resource3, Resource4]
 }
 
-@aws.iam#iamResource(name: "ResourceTwo")
+@iamResource(name: "ResourceTwo")
 resource Resource2 {
   identifiers: {
     id1: ArnString,
@@ -36,6 +43,27 @@ resource Resource2 {
   list: ListResource2,
 }
 
+@disableConditionKeyInference
+@iamResource(disableConditionKeyInheritance: true)
+resource Resource3 {
+  identifiers: {
+    id1: ArnString
+    id2: FooString
+    id3: String
+  }
+}
+
+@disableConditionKeyInference
+@iamResource(disableConditionKeyInheritance: true)
+@conditionKeys(["foo:baz"])
+resource Resource4 {
+  identifiers: {
+    id1: ArnString
+    id2: FooString
+    id4: String
+  }
+}
+
 @readonly
 operation GetResource2 {
     input: GetResource2Input
@@ -65,5 +93,5 @@ structure ListResource2Input {
 
 structure ListResource2Output {}
 
-@aws.api#arnReference(type: "ec2:Instance")
+@arnReference(type: "ec2:Instance")
 string ArnString