diff --git a/docs/source-2.0/aws/aws-iam.rst b/docs/source-2.0/aws/aws-iam.rst index d1556ead369..f2e30b8854d 100644 --- a/docs/source-2.0/aws/aws-iam.rst +++ b/docs/source-2.0/aws/aws-iam.rst @@ -17,12 +17,278 @@ policies for resources in a service. Condition keys for services defined in Smithy are automatically inferred. These can be disabled or augmented. For more information, see :ref:`deriving-condition-keys`. + +.. smithy-trait:: aws.iam#supportedPrincipalTypes +.. _aws.iam#supportedPrincipalTypes-trait: + +----------------------------------------- +``aws.iam#supportedPrincipalTypes`` trait +----------------------------------------- + +Summary + The `IAM principal types`_ that can use the service or operation. +Trait selector + ``:test(service, operation)`` +Value type + ``list`` where each string is an IAM principal type: ``Root``, + ``IAMUser``, ``IAMRole``, or ``FederatedUser``. + +Operations that are not annotated with the ``supportedPrincipalTypes`` trait +inherit the ``supportedPrincipalTypes`` of the service they are bound to. + +The following example defines two operations: + +* OperationA defines an explicit list of the IAM principal types it supports + using the ``supportedPrincipalTypes`` trait. +* OperationB is not annotated with the ``supportedPrincipalTypes`` trait, so + the IAM principal types supported by this operation are the principal types + applied to the service. + +.. code-block:: smithy + + $version: "2" + + namespace smithy.example + + use aws.iam#supportedPrincipalTypes + + @supportedPrincipalTypes(["Root", "IAMUser", "IAMRole", "FederatedUser"]) + service MyService { + version: "2020-07-02" + operations: [OperationA, OperationB] + } + + @supportedPrincipalTypes(["Root"]) + operation OperationA {} + + operation OperationB {} + + +.. _aws-iam_traits-resources: + +--------------- +Resource traits +--------------- + +.. smithy-trait:: aws.iam#iamResource +.. _aws.iam#iamResource-trait: + +``aws.iam#iamResource`` trait +============================= + +Summary + Indicates properties of a Smithy resource in AWS IAM. +Trait selector + ``resource`` +Value type + ``structure`` + +The ``aws.iam#iamResource`` trait is a structure that supports the following +members: + +.. list-table:: + :header-rows: 1 + :widths: 10 20 70 + + * - Property + - Type + - Description + * - name + - ``string`` + - The name of the resource in AWS IAM. + * - relativeDocumentation + - ``string`` + - A relative URL path that defines more information about the resource + within a set of IAM-related documentation. + +The following example defines a simple resource with a name in AWS IAM that +deviates from the :ref:`shape name of the shape ID ` of the resource. + +.. code-block:: smithy + + $version: "2" + + namespace smithy.example + + use aws.iam#iamResource + + @iamResource(name: "super") + resource SuperResource { + identifiers: { + superId: String, + } + } + + +.. _aws-iam_traits-actions: + +------------- +Action traits +------------- + +.. smithy-trait:: aws.iam#iamAction +.. _aws.iam#iamAction-trait: + +``aws.iam#iamAction`` trait +=========================== + +Summary + Indicates properties of a Smithy operation in AWS IAM. +Trait selector + ``operation`` +Value type + ``structure`` + +The ``aws.iam#iamAction`` trait is a structure that supports the following +members: + +.. list-table:: + :header-rows: 1 + :widths: 10 20 70 + + * - Property + - Type + - Description + * - name + - ``string`` + - The name of the resource in AWS IAM. + * - documentation + - ``string`` + - A brief description of what granting the user permission to invoke an + operation would entail. + * - relativeDocumentation + - ``string`` + - A relative URL path that defines more information about the operation + within a set of IAM-related documentation. + * - requiredActions + - ``list`` where each string value is the name of another action. + - The list of actions that the invoker must be authorized to perform when + executing the targeted operation. + * - resources + - `ActionResources object`_ + - The resources an IAM action can be authorized against. + * - createsResources + - ``list`` where each string value is the name of a resource. + - The list of resources that performing this IAM action will create. + +The following example defines a simple operation with a name in AWS IAM that +deviates from the :ref:`shape name of the shape ID ` of the operation. + +.. code-block:: smithy + + $version: "2" + + namespace smithy.example + + use aws.iam#iamAction + + @iamAction(name: "PutEvent") + operation OperationA {} + + +.. _aws.iam#iamAction-trait-ActionResources: + +``ActionResources`` object +-------------------------- + +The ``ActionResources`` object is a container for information on the resources +that an IAM action may be authorized against. The ``ActionResources`` object +contains the following properties: + +.. list-table:: + :header-rows: 1 + :widths: 10 20 70 + + * - Property + - Type + - Description + * - required + - ``map`` of resource name to `ActionResource object`_ + - Resources that will always be authorized against for functionality of + the IAM action. If this member is present, all inferred required + resources are ignored. + * - optional + - ``map`` of resource name to `ActionResource object`_ + - Resources that will conditionally be authorized against for + functionality of the IAM action. If this member is present, all + inferred optional resources are ignored. + + +.. _aws.iam#iamAction-trait-ActionResource: + +``ActionResource`` object +------------------------- + +The ``ActionResource`` object is a container for information about a resource +that an IAM action can be authorized against. The ``ActionResource`` object +contains the following properties: + +.. list-table:: + :header-rows: 1 + :widths: 10 20 70 + + * - Property + - Type + - Description + * - conditionKeys + - ``list`` + - The condition keys used for authorizing against this resource. + + +.. smithy-trait:: aws.iam#actionName +.. _aws.iam#actionName-trait: + +``aws.iam#actionName`` trait +============================ + +.. danger:: + This trait is deprecated. The ``name`` property of the + :ref:`aws.iam#iamAction-trait` should be used instead. + +Summary + Provides a custom IAM action name. +Trait selector + ``operation`` +Value type + ``string`` + +Operations not annotated with the ``actionName`` trait, default to the +:ref:`shape name of the shape ID ` of the targeted operation. + +The following example defines two operations: + +* ``OperationA`` is not annotated with the ``actionName`` trait, and + resolves the action name of ``OperationA``. +* ``OperationB`` has the ``actionName`` trait, so has the action + name ``OverridingActionName``. + +.. code-block:: smithy + + $version: "2" + + namespace smithy.example + + use aws.iam#actionName + + service MyService { + version: "2020-07-02" + operations: [OperationA, OperationB] + } + + operation OperationA {} + + @actionName("OverridingActionName") + operation OperationB {} + .. smithy-trait:: aws.iam#actionPermissionDescription .. _aws.iam#actionPermissionDescription-trait: ---------------------------------------------- ``aws.iam#actionPermissionDescription`` trait ---------------------------------------------- +============================================= + +.. danger:: + This trait is deprecated. The ``documentation`` property of the + :ref:`aws.iam#iamAction-trait` should be used instead. Summary A brief description of what granting the user permission to invoke an @@ -43,27 +309,31 @@ Value type @actionPermissionDescription("This will allow the user to Foo.") operation FooOperation {} -.. smithy-trait:: aws.iam#conditionKeys -.. _aws.iam#conditionKeys-trait: -------------------------------- -``aws.iam#conditionKeys`` trait -------------------------------- +.. smithy-trait:: aws.iam#requiredActions +.. _aws.iam#requiredActions-trait: + +``aws.iam#requiredActions`` trait +================================= + +.. danger:: + This trait is deprecated. The ``requiredActions`` property of the + :ref:`aws.iam#iamAction-trait` should be used instead. Summary - Applies condition keys, by name, to a resource or operation. + Other actions that the invoker must be authorized to perform when + executing the targeted operation. Trait selector - ``:test(resource, operation)`` + ``operation`` Value type - ``list`` - -Condition keys derived automatically can be applied to a resource or operation -explicitly. Condition keys applied this way MUST be either inferred or -explicitly defined via the :ref:`aws.iam#defineConditionKeys-trait` trait. + ``list`` where each string value references other actions + required for the service to authorize. -The following example's ``MyResource`` resource has the -``myservice:MyResourceFoo`` and ``otherservice:Bar`` condition keys. The -``MyOperation`` operation has the ``aws:region`` condition key. +Defines the actions, in addition to the targeted operation, that a user must +be authorized to execute in order invoke an operation. The following example +indicates that, in order to invoke the ``MyOperation`` operation, the invoker +must also be authorized to execute the ``otherservice:OtherOperation`` +operation for it to complete successfully. .. code-block:: smithy @@ -72,37 +342,34 @@ The following example's ``MyResource`` resource has the namespace smithy.example use aws.api#service - use aws.iam#definedContextKeys - use aws.iam#conditionKeys + use aws.iam#requiredActions @service(sdkId: "My Value", arnNamespace: "myservice") - @defineConditionKeys("otherservice:Bar": { type: "String" }) service MyService { version: "2017-02-11" resources: [MyResource] } - @conditionKeys(["otherservice:Bar"]) resource MyResource { identifiers: {foo: String} operations: [MyOperation] } - @conditionKeys(["aws:region"]) + @requiredActions(["otherservice:OtherOperation"]) operation MyOperation {} -.. note:: - Condition keys that refer to global ``"aws:*"`` keys can be referenced - without being defined on the service. +.. _aws-iam_traits-condition-keys: +-------------------- +Condition key traits +-------------------- .. smithy-trait:: aws.iam#defineConditionKeys .. _aws.iam#defineConditionKeys-trait: -------------------------------------- ``aws.iam#defineConditionKeys`` trait -------------------------------------- +===================================== Summary Defines the set of condition keys that appear within a service in @@ -174,7 +441,7 @@ Each condition key structure supports the following members: .. _condition-key-types: Condition Key Types -=================== +------------------- The following table describes the available types a condition key can have. Condition keys in IAM policies can be evaluated with `condition operators`_. @@ -215,12 +482,64 @@ Condition keys in IAM policies can be evaluated with `condition operators`_. - An unordered list of String types. +.. smithy-trait:: aws.iam#conditionKeys +.. _aws.iam#conditionKeys-trait: + +``aws.iam#conditionKeys`` trait +=============================== + +Summary + Applies condition keys, by name, to a resource or operation. +Trait selector + ``:test(resource, operation)`` +Value type + ``list`` + +Condition keys derived automatically can be applied to a resource or operation +explicitly. Condition keys applied this way MUST be either inferred or +explicitly defined via the :ref:`aws.iam#defineConditionKeys-trait` trait. + +The following example's ``MyResource`` resource has the +``myservice:MyResourceFoo`` and ``otherservice:Bar`` condition keys. The +``MyOperation`` operation has the ``aws:region`` condition key. + +.. code-block:: smithy + + $version: "2" + + namespace smithy.example + + use aws.api#service + use aws.iam#definedContextKeys + use aws.iam#conditionKeys + + @service(sdkId: "My Value", arnNamespace: "myservice") + @defineConditionKeys("otherservice:Bar": { type: "String" }) + service MyService { + version: "2017-02-11" + resources: [MyResource] + } + + @conditionKeys(["otherservice:Bar"]) + resource MyResource { + identifiers: {foo: String} + operations: [MyOperation] + } + + @conditionKeys(["aws:region"]) + operation MyOperation {} + +.. note:: + + Condition keys that refer to global ``"aws:*"`` keys can be referenced + without being defined on the service. + + .. smithy-trait:: aws.iam#disableConditionKeyInference .. _aws.iam#disableConditionKeyInference-trait: ----------------------------------------------- ``aws.iam#disableConditionKeyInference`` trait ----------------------------------------------- +============================================== Summary Declares that the condition keys of a resource should not be inferred. @@ -296,195 +615,11 @@ condition key inference disabled. } } -.. smithy-trait:: aws.iam#requiredActions -.. _aws.iam#requiredActions-trait: - ---------------------------------- -``aws.iam#requiredActions`` trait ---------------------------------- - -Summary - Other actions that the invoker must be authorized to perform when - executing the targeted operation. -Trait selector - ``operation`` -Value type - ``list`` where each string value references condition keys - defined in the closure of the service. - -Defines the actions, in addition to the targeted operation, that a user must -be authorized to execute in order invoke an operation. The following example -indicates that, in order to invoke the ``MyOperation`` operation, the invoker -must also be authorized to execute the ``otherservice:OtherOperation`` -operation for it to complete successfully. - -.. code-block:: smithy - - $version: "2" - - namespace smithy.example - - use aws.api#service - use aws.iam#requiredActions - - @service(sdkId: "My Value", arnNamespace: "myservice") - service MyService { - version: "2017-02-11" - resources: [MyResource] - } - - resource MyResource { - identifiers: {foo: String} - operations: [MyOperation] - } - - @requiredActions(["otherservice:OtherOperation"]) - operation MyOperation {} - -.. smithy-trait:: aws.iam#supportedPrincipalTypes -.. _aws.iam#supportedPrincipalTypes-trait: - ------------------------------------------ -``aws.iam#supportedPrincipalTypes`` trait ------------------------------------------ - -Summary - The `IAM principal types`_ that can use the service or operation. -Trait selector - ``:test(service, operation)`` -Value type - ``list`` where each string is an IAM principal type: ``Root``, - ``IAMUser``, ``IAMRole``, or ``FederatedUser``. - -Operations that are not annotated with the ``supportedPrincipalTypes`` trait -inherit the ``supportedPrincipalTypes`` of the service they are bound to. - -The following example defines two operations: - -* OperationA defines an explicit list of the IAM principal types it supports - using the ``supportedPrincipalTypes`` trait. -* OperationB is not annotated with the ``supportedPrincipalTypes`` trait, so - the IAM principal types supported by this operation are the principal types - applied to the service. - -.. code-block:: smithy - - $version: "2" - - namespace smithy.example - - use aws.iam#supportedPrincipalTypes - - @supportedPrincipalTypes(["Root", "IAMUser", "IAMRole", "FederatedUser"]) - service MyService { - version: "2020-07-02" - operations: [OperationA, OperationB] - } - - @supportedPrincipalTypes(["Root"]) - operation OperationA {} - - operation OperationB {} - - -.. smithy-trait:: aws.iam#iamResource -.. _aws.iam#iamResource-trait: - ------------------------------ -``aws.iam#iamResource`` trait ------------------------------ - -Summary - Indicates properties of a Smithy resource in AWS IAM. -Trait selector - ``resource`` -Value type - ``structure`` - -The ``aws.iam#iamResource`` trait is a structure that supports the following -members: - -.. list-table:: - :header-rows: 1 - :widths: 10 20 70 - - * - Property - - Type - - Description - * - name - - ``string`` - - The name of the resource in AWS IAM. - * - relativeDocumentation - - ``string`` - - A relative URL path that defines more information about the resource - within a set of IAM-related documentation. - -The following example defines a simple resource with a name in AWS IAM that -deviates from the :ref:`shape name of the shape ID ` of the resource. - -.. code-block:: smithy - - $version: "2" - - namespace smithy.example - - use aws.iam#iamResource - - @iamResource(name: "super") - resource SuperResource { - identifiers: { - superId: String, - } - } - -.. smithy-trait:: aws.iam#actionName -.. _aws.iam#actionName-trait: - ----------------------------- -``aws.iam#actionName`` trait ----------------------------- - -Summary - Provides a custom IAM action name. -Trait selector - ``operation`` -Value type - ``string`` - -Operations not annotated with the ``actionName`` trait, default to the -:ref:`shape name of the shape ID ` of the targeted operation. - -The following example defines two operations: - -* ``OperationA`` is not annotated with the ``actionName`` trait, and - resolves the action name of ``OperationA``. -* ``OperationB`` has the ``actionName`` trait, so has the action - name ``OverridingActionName``. - -.. code-block:: smithy - - $version: "2" - - namespace smithy.example - - use aws.iam#actionName - - service MyService { - version: "2020-07-02" - operations: [OperationA, OperationB] - } - - operation OperationA {} - - @actionName("OverridingActionName") - operation OperationB {} - .. smithy-trait:: aws.iam#serviceResolvedConditionKeys .. _aws.iam#serviceResolvedConditionKeys-trait: ----------------------------------------------- ``aws.iam#serviceResolvedConditionKeys`` trait ----------------------------------------------- +============================================== Summary Specifies the list of IAM condition keys which must be resolved by the @@ -524,9 +659,8 @@ The following example defines two service-specific condition keys: .. smithy-trait:: aws.iam#conditionKeyValue .. _aws.iam#conditionKeyValue-trait: ------------------------------------ ``aws.iam#conditionKeyValue`` trait ------------------------------------ +=================================== Summary Uses the associated member’s value for the specified condition key. @@ -572,9 +706,8 @@ explicitly binds ``ActionContextKey1`` to the field ``key``. .. _deriving-condition-keys: ------------------------ Deriving Condition Keys ------------------------ +======================= Smithy will automatically derive condition key information for a service, as well as its resources and operations. diff --git a/smithy-aws-iam-traits/src/main/java/software/amazon/smithy/aws/iam/traits/ActionResource.java b/smithy-aws-iam-traits/src/main/java/software/amazon/smithy/aws/iam/traits/ActionResource.java new file mode 100644 index 00000000000..9c9708e5a2c --- /dev/null +++ b/smithy-aws-iam-traits/src/main/java/software/amazon/smithy/aws/iam/traits/ActionResource.java @@ -0,0 +1,112 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.aws.iam.traits; + +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import software.amazon.smithy.model.node.ArrayNode; +import software.amazon.smithy.model.node.Node; +import software.amazon.smithy.model.node.ObjectNode; +import software.amazon.smithy.model.node.StringNode; +import software.amazon.smithy.model.node.ToNode; +import software.amazon.smithy.utils.BuilderRef; +import software.amazon.smithy.utils.SmithyBuilder; +import software.amazon.smithy.utils.ToSmithyBuilder; + +/** + * Contains information about a resource an IAM action can be authorized against. + */ +public final class ActionResource implements ToNode, ToSmithyBuilder { + private static final String CONDITION_KEYS = "conditionKeys"; + + private final List conditionKeys; + + private ActionResource(Builder builder) { + this.conditionKeys = builder.conditionKeys.copy(); + } + + /** + * Gets the condition keys used for authorizing against this resource. + * + * @return the condition keys. + */ + public List getConditionKeys() { + return conditionKeys; + } + + public static Builder builder() { + return new Builder(); + } + + public static ActionResource fromNode(Node value) { + Builder builder = builder(); + value.expectObjectNode() + .warnIfAdditionalProperties(Collections.singletonList(CONDITION_KEYS)) + .getArrayMember(CONDITION_KEYS, StringNode::getValue, builder::conditionKeys); + return builder.build(); + } + + @Override + public Node toNode() { + ObjectNode.Builder builder = Node.objectNodeBuilder(); + if (!conditionKeys.isEmpty()) { + builder.withMember(CONDITION_KEYS, ArrayNode.fromStrings(conditionKeys)); + } + return builder.build(); + } + + @Override + public SmithyBuilder toBuilder() { + return builder().conditionKeys(conditionKeys); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } else if (o == null || getClass() != o.getClass()) { + return false; + } + ActionResource that = (ActionResource) o; + return Objects.equals(conditionKeys, that.conditionKeys); + } + + @Override + public int hashCode() { + return Objects.hash(conditionKeys); + } + + public static final class Builder implements SmithyBuilder { + private final BuilderRef> conditionKeys = BuilderRef.forList(); + + @Override + public ActionResource build() { + return new ActionResource(this); + } + + public Builder conditionKeys(List conditionKeys) { + clearConditionKeys(); + this.conditionKeys.get().addAll(conditionKeys); + return this; + } + + public Builder clearConditionKeys() { + conditionKeys.get().clear(); + return this; + } + + public Builder addConditionKey(String conditionKey) { + conditionKeys.get().add(conditionKey); + return this; + } + + public Builder removeConditionKey(String conditionKey) { + conditionKeys.get().remove(conditionKey); + return this; + } + } +} diff --git a/smithy-aws-iam-traits/src/main/java/software/amazon/smithy/aws/iam/traits/ActionResources.java b/smithy-aws-iam-traits/src/main/java/software/amazon/smithy/aws/iam/traits/ActionResources.java new file mode 100644 index 00000000000..34c794b77cf --- /dev/null +++ b/smithy-aws-iam-traits/src/main/java/software/amazon/smithy/aws/iam/traits/ActionResources.java @@ -0,0 +1,165 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.aws.iam.traits; + +import java.util.Map; +import java.util.Objects; +import software.amazon.smithy.model.node.Node; +import software.amazon.smithy.model.node.ObjectNode; +import software.amazon.smithy.model.node.ToNode; +import software.amazon.smithy.utils.BuilderRef; +import software.amazon.smithy.utils.ListUtils; +import software.amazon.smithy.utils.SmithyBuilder; +import software.amazon.smithy.utils.ToSmithyBuilder; + +/** + * Contains information about the resources an IAM action can be authorized against. + */ +public final class ActionResources implements ToNode, ToSmithyBuilder { + private static final String REQUIRED = "required"; + private static final String OPTIONAL = "optional"; + + private final Map required; + private final Map optional; + + private ActionResources(Builder builder) { + required = builder.required.copy(); + optional = builder.optional.copy(); + } + + /** + * Gets the resources that will always be authorized against for + * functionality of the IAM action. + * + * @return the required resources. + */ + public Map getRequired() { + return required; + } + + /** + * Gets the resources that will conditionally be authorized against + * for functionality of the IAM action. + * + * @return the optional resources. + */ + public Map getOptional() { + return optional; + } + + private static Builder builder() { + return new Builder(); + } + + public static ActionResources fromNode(Node value) { + Builder builder = builder(); + ObjectNode node = value.expectObjectNode() + .warnIfAdditionalProperties(ListUtils.of(REQUIRED, OPTIONAL)); + if (node.containsMember(REQUIRED)) { + for (Map.Entry entry : node.expectObjectMember(REQUIRED).getStringMap().entrySet()) { + builder.putRequired(entry.getKey(), ActionResource.fromNode(entry.getValue())); + } + } + if (node.containsMember(OPTIONAL)) { + for (Map.Entry entry : node.expectObjectMember(OPTIONAL).getStringMap().entrySet()) { + builder.putOptional(entry.getKey(), ActionResource.fromNode(entry.getValue())); + } + } + return builder.build(); + } + + @Override + public Node toNode() { + ObjectNode.Builder builder = Node.objectNodeBuilder(); + if (!required.isEmpty()) { + ObjectNode.Builder requiredBuilder = Node.objectNodeBuilder(); + for (Map.Entry requiredEntry : required.entrySet()) { + requiredBuilder.withMember(requiredEntry.getKey(), requiredEntry.getValue().toNode()); + } + } + if (!optional.isEmpty()) { + ObjectNode.Builder optionalBuilder = Node.objectNodeBuilder(); + for (Map.Entry optionalEntry : optional.entrySet()) { + optionalBuilder.withMember(optionalEntry.getKey(), optionalEntry.getValue().toNode()); + } + } + return builder.build(); + } + + @Override + public SmithyBuilder toBuilder() { + return builder().required(required).optional(optional); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } else if (o == null || getClass() != o.getClass()) { + return false; + } + ActionResources that = (ActionResources) o; + return Objects.equals(required, that.required) + && Objects.equals(optional, that.optional); + } + + @Override + public int hashCode() { + return Objects.hash(required, optional); + } + + public static final class Builder implements SmithyBuilder { + private final BuilderRef> required = BuilderRef.forOrderedMap(); + private final BuilderRef> optional = BuilderRef.forOrderedMap(); + + @Override + public ActionResources build() { + return new ActionResources(this); + } + + public Builder clearRequired() { + required.get().clear(); + return this; + } + + public Builder required(Map required) { + clearRequired(); + this.required.get().putAll(required); + return this; + } + + public Builder putRequired(String resourceName, ActionResource actionResource) { + required.get().put(resourceName, actionResource); + return this; + } + + public Builder removeRequired(String resourceName) { + required.get().remove(resourceName); + return this; + } + + public Builder clearOptional() { + optional.get().clear(); + return this; + } + + public Builder optional(Map optional) { + clearOptional(); + this.optional.get().putAll(optional); + return this; + } + + public Builder putOptional(String resourceName, ActionResource actionResource) { + optional.get().put(resourceName, actionResource); + return this; + } + + public Builder removeOptional(String resourceName) { + optional.get().remove(resourceName); + return this; + } + } +} diff --git a/smithy-aws-iam-traits/src/main/java/software/amazon/smithy/aws/iam/traits/IamActionTrait.java b/smithy-aws-iam-traits/src/main/java/software/amazon/smithy/aws/iam/traits/IamActionTrait.java new file mode 100644 index 00000000000..67377e186f3 --- /dev/null +++ b/smithy-aws-iam-traits/src/main/java/software/amazon/smithy/aws/iam/traits/IamActionTrait.java @@ -0,0 +1,218 @@ +/* + * Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.smithy.aws.iam.traits; + +import java.util.List; +import java.util.Optional; +import software.amazon.smithy.model.node.Node; +import software.amazon.smithy.model.node.NodeMapper; +import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.model.traits.AbstractTrait; +import software.amazon.smithy.model.traits.AbstractTraitBuilder; +import software.amazon.smithy.model.traits.Trait; +import software.amazon.smithy.utils.BuilderRef; +import software.amazon.smithy.utils.SmithyBuilder; +import software.amazon.smithy.utils.ToSmithyBuilder; + +/** + * Indicates properties of a Smithy operation as an IAM action. + */ +public final class IamActionTrait extends AbstractTrait + implements ToSmithyBuilder { + public static final ShapeId ID = ShapeId.from("aws.iam#iamAction"); + + private final String name; + private final String documentation; + private final String relativeDocumentation; + private final List requiredActions; + private final ActionResources resources; + private final List createsResources; + + private IamActionTrait(Builder builder) { + super(ID, builder.getSourceLocation()); + name = builder.name; + documentation = builder.documentation; + relativeDocumentation = builder.relativeDocumentation; + requiredActions = builder.requiredActions.copy(); + resources = builder.resources; + createsResources = builder.createsResources.copy(); + } + + /** + * Get the AWS IAM resource name. + * + * @return Returns the name. + */ + public Optional getName() { + return Optional.ofNullable(name); + } + + /** + * Gets the description of what granting the user permission to + * invoke an operation would entail. + * + * @return Returns the documentation. + */ + public Optional getDocumentation() { + return Optional.ofNullable(documentation); + } + + /** + * Gets the relative URL path for the action within a set of + * IAM-related documentation. + * + * @return Returns the relative URL path to documentation. + */ + public Optional getRelativeDocumentation() { + return Optional.ofNullable(relativeDocumentation); + } + + /** + * Gets other actions that the invoker must be authorized to + * perform when executing the targeted operation. + * + * @return Returns the list of required actions. + */ + public List getRequiredActions() { + return requiredActions; + } + + /** + * Gets the resources an IAM action can be authorized against. + * + * @return Returns the action's resources. + */ + public Optional getResources() { + return Optional.ofNullable(resources); + } + + /** + * Gets the resources that performing this IAM action will create. + * + * @return Returns the resources created by the action. + */ + public List getCreatesResources() { + return createsResources; + } + + public static Builder builder() { + return new Builder(); + } + + @Override + protected Node createNode() { + NodeMapper mapper = new NodeMapper(); + mapper.disableToNodeForClass(IamActionTrait.class); + mapper.setOmitEmptyValues(true); + return mapper.serialize(this).expectObjectNode(); + } + + @Override + public SmithyBuilder toBuilder() { + return builder().sourceLocation(getSourceLocation()).name(name); + } + + public static final class Provider extends AbstractTrait.Provider { + public Provider() { + super(ID); + } + + @Override + public Trait createTrait(ShapeId target, Node value) { + IamActionTrait result = new NodeMapper().deserialize(value, IamActionTrait.class); + result.setNodeCache(value); + return result; + } + } + + public static final class Builder extends AbstractTraitBuilder { + private String name; + private String documentation; + private String relativeDocumentation; + private final BuilderRef> requiredActions = BuilderRef.forList(); + private ActionResources resources; + private final BuilderRef> createsResources = BuilderRef.forList(); + + private Builder() {} + + @Override + public IamActionTrait build() { + return new IamActionTrait(this); + } + + public Builder name(String name) { + this.name = name; + return this; + } + + public Builder documentation(String documentation) { + this.documentation = documentation; + return this; + } + + public Builder relativeDocumentation(String relativeDocumentation) { + this.relativeDocumentation = relativeDocumentation; + return this; + } + + public Builder requiredActions(List requiredActions) { + clearRequiredActions(); + this.requiredActions.get().addAll(requiredActions); + return this; + } + + public Builder clearRequiredActions() { + requiredActions.get().clear(); + return this; + } + + public Builder addRequiredAction(String requiredAction) { + requiredActions.get().add(requiredAction); + return this; + } + + public Builder removeRequiredAction(String requiredAction) { + requiredActions.get().remove(requiredAction); + return this; + } + + public Builder resources(ActionResources resources) { + this.resources = resources; + return this; + } + + public Builder createsResources(List createsResources) { + clearCreatesResources(); + this.createsResources.get().addAll(createsResources); + return this; + } + + public Builder clearCreatesResources() { + createsResources.get().clear(); + return this; + } + + public Builder addCreatesResource(String createsResource) { + createsResources.get().add(createsResource); + return this; + } + + public Builder removeCreatesResource(String createsResource) { + createsResources.get().remove(createsResource); + return this; + } + } +} diff --git a/smithy-aws-iam-traits/src/main/java/software/amazon/smithy/aws/iam/traits/IamActionValidator.java b/smithy-aws-iam-traits/src/main/java/software/amazon/smithy/aws/iam/traits/IamActionValidator.java new file mode 100644 index 00000000000..b04a2a26889 --- /dev/null +++ b/smithy-aws-iam-traits/src/main/java/software/amazon/smithy/aws/iam/traits/IamActionValidator.java @@ -0,0 +1,80 @@ +/* + * 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 java.lang.String.format; + +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.shapes.OperationShape; +import software.amazon.smithy.model.traits.Trait; +import software.amazon.smithy.model.validation.AbstractValidator; +import software.amazon.smithy.model.validation.ValidationEvent; +import software.amazon.smithy.utils.SmithyInternalApi; + +@SmithyInternalApi +public final class IamActionValidator extends AbstractValidator { + @Override + public List validate(Model model) { + List events = new ArrayList<>(); + for (OperationShape operation : model.getOperationShapesWithTrait(IamActionTrait.class)) { + IamActionTrait trait = operation.expectTrait(IamActionTrait.class); + events.addAll(validateDuplicateTraits(operation, trait)); + validateUniqueResourceNames(operation, trait).ifPresent(events::add); + } + return events; + } + + private List validateDuplicateTraits(OperationShape operation, IamActionTrait trait) { + List events = new ArrayList<>(); + if (operation.hasTrait(ActionNameTrait.ID) && trait.getName().isPresent()) { + events.add(emitDeprecatedOverride(operation, + operation.expectTrait(ActionNameTrait.class), + "name")); + } + + if (operation.hasTrait(ActionPermissionDescriptionTrait.ID) && trait.getDocumentation().isPresent()) { + events.add(emitDeprecatedOverride(operation, + operation.expectTrait(ActionPermissionDescriptionTrait.class), + "documentation")); + } + + if (operation.hasTrait(RequiredActionsTrait.ID) && !trait.getRequiredActions().isEmpty()) { + events.add(emitDeprecatedOverride(operation, + operation.expectTrait(RequiredActionsTrait.class), + "requiredActions")); + } + return events; + } + + private ValidationEvent emitDeprecatedOverride(OperationShape operation, Trait trait, String name) { + return error(operation, trait, format("Operation has the `%s` property of the " + + "`@aws.iam#iamAction` trait set and the deprecated `@%s` trait applied.", + name, trait.toShapeId()), "ConflictingProperty", name); + } + + private Optional validateUniqueResourceNames(OperationShape operation, IamActionTrait trait) { + if (!trait.getResources().isPresent() + || trait.getResources().get().getRequired().isEmpty() + || trait.getResources().get().getOptional().isEmpty() + ) { + return Optional.empty(); + } + + Set duplicateNames = new LinkedHashSet<>(trait.getResources().get().getRequired().keySet()); + duplicateNames.retainAll(trait.getResources().get().getOptional().keySet()); + if (duplicateNames.isEmpty()) { + return Optional.empty(); + } + + return Optional.of(danger(operation, trait, "Operation has the following resource names defined as both " + + "required and optional: " + duplicateNames, "Resources", "DuplicateEntries")); + } +} diff --git a/smithy-aws-iam-traits/src/main/resources/META-INF/services/software.amazon.smithy.model.traits.TraitService b/smithy-aws-iam-traits/src/main/resources/META-INF/services/software.amazon.smithy.model.traits.TraitService index 2853cf73ca0..7dfe76966af 100644 --- a/smithy-aws-iam-traits/src/main/resources/META-INF/services/software.amazon.smithy.model.traits.TraitService +++ b/smithy-aws-iam-traits/src/main/resources/META-INF/services/software.amazon.smithy.model.traits.TraitService @@ -4,6 +4,7 @@ software.amazon.smithy.aws.iam.traits.DefineConditionKeysTrait$Provider software.amazon.smithy.aws.iam.traits.DisableConditionKeyInferenceTrait$Provider software.amazon.smithy.aws.iam.traits.RequiredActionsTrait$Provider software.amazon.smithy.aws.iam.traits.SupportedPrincipalTypesTrait$Provider +software.amazon.smithy.aws.iam.traits.IamActionTrait$Provider software.amazon.smithy.aws.iam.traits.IamResourceTrait$Provider software.amazon.smithy.aws.iam.traits.ActionNameTrait$Provider software.amazon.smithy.aws.iam.traits.ServiceResolvedConditionKeysTrait$Provider 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 d03291f5128..856e5dbe68a 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,2 +1,3 @@ software.amazon.smithy.aws.iam.traits.ConditionKeysValidator +software.amazon.smithy.aws.iam.traits.IamActionValidator software.amazon.smithy.aws.iam.traits.IamResourceTraitValidator 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 ae2ca3b5eed..2423f7dfc3a 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 @@ -3,11 +3,13 @@ $version: "2.0" namespace aws.iam /// Provides a custom IAM action name. By default, the action name is the same as the operation name. +@deprecated(since: "2023-10-31", message: "Use the `name` member of the `aws.iam#iamAction` trait instead.") @trait(selector: "operation") string actionName /// A brief description of what granting the user permission to invoke an operation would entail. /// This description should begin with something similar to 'Enables the user to...' or 'Grants permission to...' +@deprecated(since: "2023-10-31", message: "Use the `documentation` member of the `aws.iam#iamAction` trait instead.") @trait(selector: "operation") string actionPermissionDescription @@ -34,6 +36,29 @@ map defineConditionKeys { @trait(selector: ":test(service, resource)") structure disableConditionKeyInference {} +/// Indicates properties of a Smithy operation as an IAM action. +@trait(selector: "operation") +structure iamAction { + /// The name of the action in AWS IAM. + name: String + + /// A brief description of what granting the user permission to invoke an operation would entail. + /// This description should begin with something similar to 'Enables the user to...' or 'Grants permission to...' + documentation: String + + /// A relative URL path that defines more information about the action within a set of IAM-related documentation. + relativeDocumentation: String + + /// Other actions that the invoker must be authorized to perform when executing the targeted operation. + requiredActions: RequiredActionsList + + /// The resources an IAM action can be authorized against. + resources: ActionResources + + /// The resources that performing this IAM action will create. + createsResources: ResourceNameList +} + /// Indicates properties of a Smithy resource in AWS IAM. @trait(selector: "resource") structure iamResource { @@ -46,6 +71,7 @@ structure iamResource { } /// Other actions that the invoker must be authorized to perform when executing the targeted operation. +@deprecated(since: "2023-10-31", message: "Use the `requiredActions` member of the `aws.iam#iamAction` trait instead.") @trait(selector: "operation") list requiredActions { member: IamIdentifier @@ -64,6 +90,16 @@ list supportedPrincipalTypes { member: PrincipalType } +/// A container for information on the resources that an IAM action may be authorized against. +@private +structure ActionResources { + /// Resources that will always be authorized against for functionality of the IAM action. + required: ActionResourceMap + + /// Resources that will conditionally be authorized against for functionality of the IAM action. + optional: ActionResourceMap +} + /// A defined condition key to appear within a service in addition to inferred and global condition keys. @private structure ConditionKeyDefinition { @@ -81,6 +117,37 @@ structure ConditionKeyDefinition { relativeDocumentation: String } +/// Contains information about a resource an IAM action can be authorized against. +@private +structure ActionResource { + /// The condition keys used for authorizing against this resource. + conditionKeys: ConditionKeysList +} + +@private +map ActionResourceMap { + key: ResourceName + value: ActionResource +} + +@private +@uniqueItems +list ConditionKeysList { + member: String +} + +@private +@uniqueItems +list RequiredActionsList { + member: IamIdentifier +} + +@private +@uniqueItems +list ResourceNameList { + member: ResourceName +} + /// The IAM policy type of the value that will supplied for this context key @private enum ConditionKeyType { @@ -131,6 +198,9 @@ enum ConditionKeyType { @private string IamIdentifier +@private +string ResourceName + /// An IAM policy principal type. @private enum PrincipalType { diff --git a/smithy-aws-iam-traits/src/test/java/software/amazon/smithy/aws/iam/traits/IamActionTraitTest.java b/smithy-aws-iam-traits/src/test/java/software/amazon/smithy/aws/iam/traits/IamActionTraitTest.java new file mode 100644 index 00000000000..e0cad6b3e78 --- /dev/null +++ b/smithy-aws-iam-traits/src/test/java/software/amazon/smithy/aws/iam/traits/IamActionTraitTest.java @@ -0,0 +1,57 @@ +/* + * Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.smithy.aws.iam.traits; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.shapes.Shape; +import software.amazon.smithy.model.shapes.ShapeId; + +public class IamActionTraitTest { + @Test + public void loadsFromModel() { + Model result = Model.assembler() + .discoverModels(getClass().getClassLoader()) + .addImport(getClass().getResource("iam-action.smithy")) + .assemble() + .unwrap(); + + Shape fooOperation = result.expectShape(ShapeId.from("smithy.example#Foo")); + + assertTrue(fooOperation.hasTrait(IamActionTrait.class)); + IamActionTrait trait = fooOperation.expectTrait(IamActionTrait.class); + assertEquals(trait.getName().get(), "foo"); + assertEquals(trait.getDocumentation().get(), "docs"); + assertEquals(trait.getRelativeDocumentation().get(), "page.html#actions"); + assertThat(trait.getRequiredActions(), contains("iam:PassRole", "ec2:RunInstances")); + assertThat(trait.getCreatesResources(), contains("kettle")); + + assertTrue(trait.getResources().isPresent()); + ActionResources actionResources = trait.getResources().get(); + assertTrue(actionResources.getRequired().containsKey("bar")); + assertThat(actionResources.getRequired().get("bar").getConditionKeys(), contains("foo:asdf")); + assertTrue(actionResources.getRequired().containsKey("bap")); + assertThat(actionResources.getRequired().get("bap").getConditionKeys(), contains("foo:zxcv", "foo:hjkl")); + assertTrue(actionResources.getOptional().containsKey("baz")); + assertTrue(actionResources.getOptional().get("baz").getConditionKeys().isEmpty()); + } +} diff --git a/smithy-aws-iam-traits/src/test/java/software/amazon/smithy/aws/iam/traits/SmithyErrorFilesTest.java b/smithy-aws-iam-traits/src/test/java/software/amazon/smithy/aws/iam/traits/SmithyErrorFilesTest.java deleted file mode 100644 index 67bf4d3b45a..00000000000 --- a/smithy-aws-iam-traits/src/test/java/software/amazon/smithy/aws/iam/traits/SmithyErrorFilesTest.java +++ /dev/null @@ -1,21 +0,0 @@ -package software.amazon.smithy.aws.iam.traits; - -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.MethodSource; -import software.amazon.smithy.model.validation.testrunner.SmithyTestCase; -import software.amazon.smithy.model.validation.testrunner.SmithyTestSuite; - -import java.util.concurrent.Callable; -import java.util.stream.Stream; - -public class SmithyErrorFilesTest { - @ParameterizedTest(name = "{0}") - @MethodSource("source") - public void testRunner(String filename, Callable callable) throws Exception { - callable.call(); - } - - public static Stream source() { - return SmithyTestSuite.defaultParameterizedTestSource(SmithyErrorFilesTest.class); - } -} diff --git a/smithy-aws-iam-traits/src/test/resources/software/amazon/smithy/aws/iam/traits/errorfiles/iam-action-conflicts.errors b/smithy-aws-iam-traits/src/test/resources/software/amazon/smithy/aws/iam/traits/errorfiles/iam-action-conflicts.errors new file mode 100644 index 00000000000..51dfda99edf --- /dev/null +++ b/smithy-aws-iam-traits/src/test/resources/software/amazon/smithy/aws/iam/traits/errorfiles/iam-action-conflicts.errors @@ -0,0 +1,3 @@ +[ERROR] smithy.example#Foo: Operation has the `name` property of the `@aws.iam#iamAction` trait set and the deprecated `@aws.iam#actionName` trait applied. | IamAction.ConflictingProperty.name +[ERROR] smithy.example#Foo: Operation has the `documentation` property of the `@aws.iam#iamAction` trait set and the deprecated `@aws.iam#actionPermissionDescription` trait applied. | IamAction.ConflictingProperty.documentation +[ERROR] smithy.example#Foo: Operation has the `requiredActions` property of the `@aws.iam#iamAction` trait set and the deprecated `@aws.iam#requiredActions` trait applied. | IamAction.ConflictingProperty.requiredActions diff --git a/smithy-aws-iam-traits/src/test/resources/software/amazon/smithy/aws/iam/traits/errorfiles/iam-action-conflicts.smithy b/smithy-aws-iam-traits/src/test/resources/software/amazon/smithy/aws/iam/traits/errorfiles/iam-action-conflicts.smithy new file mode 100644 index 00000000000..f22f066b986 --- /dev/null +++ b/smithy-aws-iam-traits/src/test/resources/software/amazon/smithy/aws/iam/traits/errorfiles/iam-action-conflicts.smithy @@ -0,0 +1,18 @@ +$version: "2.0" + +namespace smithy.example + +use aws.iam#actionName +use aws.iam#actionPermissionDescription +use aws.iam#iamAction +use aws.iam#requiredActions + +@iamAction( + name: "foo" + documentation: "docs" + requiredActions: ["foo:Bar"] +) +@actionName("foo") +@actionPermissionDescription("docs") +@requiredActions(["foo:Bar"]) +operation Foo {} diff --git a/smithy-aws-iam-traits/src/test/resources/software/amazon/smithy/aws/iam/traits/errorfiles/iam-action-resource-detachment.errors b/smithy-aws-iam-traits/src/test/resources/software/amazon/smithy/aws/iam/traits/errorfiles/iam-action-resource-detachment.errors new file mode 100644 index 00000000000..e69de29bb2d diff --git a/smithy-aws-iam-traits/src/test/resources/software/amazon/smithy/aws/iam/traits/errorfiles/iam-action-resource-detachment.smithy b/smithy-aws-iam-traits/src/test/resources/software/amazon/smithy/aws/iam/traits/errorfiles/iam-action-resource-detachment.smithy new file mode 100644 index 00000000000..4d63061516e --- /dev/null +++ b/smithy-aws-iam-traits/src/test/resources/software/amazon/smithy/aws/iam/traits/errorfiles/iam-action-resource-detachment.smithy @@ -0,0 +1,23 @@ +$version: "2.0" + +namespace smithy.example + +use aws.iam#iamAction + +resource Monitor { + resources: [HealthEvent] +} + +resource HealthEvent { + read: GetHealthEvent +} + +@iamAction( + resources: { + required: { + "HealthEvent": {} + } + } +) +@readonly +operation GetHealthEvent {} diff --git a/smithy-aws-iam-traits/src/test/resources/software/amazon/smithy/aws/iam/traits/errorfiles/iam-action-resource-duplicates.errors b/smithy-aws-iam-traits/src/test/resources/software/amazon/smithy/aws/iam/traits/errorfiles/iam-action-resource-duplicates.errors new file mode 100644 index 00000000000..7473735a040 --- /dev/null +++ b/smithy-aws-iam-traits/src/test/resources/software/amazon/smithy/aws/iam/traits/errorfiles/iam-action-resource-duplicates.errors @@ -0,0 +1 @@ +[DANGER] smithy.example#Foo: Operation has the following resource names defined as both required and optional: [bap] | IamAction.Resources.DuplicateEntries diff --git a/smithy-aws-iam-traits/src/test/resources/software/amazon/smithy/aws/iam/traits/errorfiles/iam-action-resource-duplicates.smithy b/smithy-aws-iam-traits/src/test/resources/software/amazon/smithy/aws/iam/traits/errorfiles/iam-action-resource-duplicates.smithy new file mode 100644 index 00000000000..d83a54be105 --- /dev/null +++ b/smithy-aws-iam-traits/src/test/resources/software/amazon/smithy/aws/iam/traits/errorfiles/iam-action-resource-duplicates.smithy @@ -0,0 +1,27 @@ +$version: "2.0" + +namespace smithy.example + +use aws.iam#iamAction + +@iamAction( + resources: { + required: { + "bar": { + conditionKeys: ["foo:asdf"] + } + "bap": { + conditionKeys: ["foo:zxcv"] + } + } + optional: { + "baz": { + conditionKeys: ["foo:qwer"] + } + "bap": { + conditionKeys: ["foo:zxcv"] + } + } + } +) +operation Foo {} diff --git a/smithy-aws-iam-traits/src/test/resources/software/amazon/smithy/aws/iam/traits/iam-action.smithy b/smithy-aws-iam-traits/src/test/resources/software/amazon/smithy/aws/iam/traits/iam-action.smithy new file mode 100644 index 00000000000..b52ac3f6fba --- /dev/null +++ b/smithy-aws-iam-traits/src/test/resources/software/amazon/smithy/aws/iam/traits/iam-action.smithy @@ -0,0 +1,26 @@ +$version: "2.0" + +namespace smithy.example + +use aws.iam#iamAction + +@iamAction(name: "foo" + documentation: "docs" + relativeDocumentation: "page.html#actions" + requiredActions: ["iam:PassRole", "ec2:RunInstances"] + resources: { + required: { + "bar": { + conditionKeys: ["foo:asdf"] + } + "bap": { + conditionKeys: ["foo:zxcv", "foo:hjkl"] + } + } + optional: { + "baz": {} + } + } + createsResources: ["kettle"] +) +operation Foo {}