Skip to content

Commit

Permalink
Add conditionKeyValue and serviceResolvedConditionKeys (#1677)
Browse files Browse the repository at this point in the history
* Adding conditionKeyValue and serviceResolvedConditionKeys traits
* Update trait constructors to be public
  • Loading branch information
0xjjoyy authored Aug 18, 2023
1 parent 7bf6e00 commit b5b708a
Show file tree
Hide file tree
Showing 24 changed files with 441 additions and 13 deletions.
90 changes: 90 additions & 0 deletions docs/source-2.0/aws/aws-iam.rst
Original file line number Diff line number Diff line change
Expand Up @@ -434,6 +434,96 @@ The following example defines two operations:
@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
service, as opposed to the value being pulled from the request.
Trait selector
``service``
Value type
``list<string>``

All condition keys defined with the ``serviceResolvedConditionKeys`` trait
MUST also be defined via the :ref:`aws.iam#defineConditionKeys-trait` trait.
Derived resource condition keys MUST NOT be included
with the ``serviceResolvedConditionKeys`` trait.

The following example defines two service-specific condition keys:

* ``myservice:ActionContextKey1`` is expected to be resolved by the service.
* ``myservice:ActionContextKey2`` is expected to be pulled from the request.

.. code-block:: smithy
$version: "2"
namespace smithy.example
@defineConditionKeys(
"myservice:ActionContextKey1": { type: "String" },
"myservice:ActionContextKey2": { type: "String" }
)
@serviceResolvedConditionKeys(["myservice:ActionContextKey1"])
@service(sdkId: "My Value", arnNamespace: "myservice")
service MyService {
version: "2018-05-10"
}
.. 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.
Trait selector
``member``
Value type
``string``

Members not annotated with the ``conditionKeyValue`` trait, default to the
:ref:`shape name of the shape ID <shape-id>` of the targeted member. All
condition keys defined with the ``conditionKeyValue`` trait MUST also be
defined via the :ref:`aws.iam#defineConditionKeys-trait` trait.

In the input shape for ``OperationA``, the trait ``conditionKeyValue``
explicitly binds ``ActionContextKey1`` to the field ``key``.

.. code-block:: smithy
$version: "2"
namespace smithy.example
@defineConditionKeys(
"myservice:ActionContextKey1": { type: "String" }
)
@service(sdkId: "My Value", arnNamespace: "myservice")
service MyService {
version: "2020-07-02"
operations: [OperationA]
}
@conditionKeys(["myservice:ActionContextKey1"])
operation OperationA {
input := {
@conditionKeyValue("ActionContextKey1")
key: String
}
output := {
out: String
}
}
.. _deriving-condition-keys:

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@ public final class ActionNameTrait extends StringTrait {

public static final ShapeId ID = ShapeId.from("aws.iam#actionName");

private ActionNameTrait(String action) {
public ActionNameTrait(String action) {
super(ID, action, SourceLocation.NONE);
}

private ActionNameTrait(String action, FromSourceLocation sourceLocation) {
public ActionNameTrait(String action, FromSourceLocation sourceLocation) {
super(ID, action, sourceLocation);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,11 @@
public final class ActionPermissionDescriptionTrait extends StringTrait {
public static final ShapeId ID = ShapeId.from("aws.iam#actionPermissionDescription");

private ActionPermissionDescriptionTrait(String value, SourceLocation sourceLocation) {
public ActionPermissionDescriptionTrait(String value) {
super(ID, value, SourceLocation.NONE);
}

public ActionPermissionDescriptionTrait(String value, SourceLocation sourceLocation) {
super(ID, value, sourceLocation);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/

package software.amazon.smithy.aws.iam.traits;

import software.amazon.smithy.model.FromSourceLocation;
import software.amazon.smithy.model.SourceLocation;
import software.amazon.smithy.model.shapes.ShapeId;
import software.amazon.smithy.model.traits.StringTrait;

public final class ConditionKeyValueTrait extends StringTrait {
public static final ShapeId ID = ShapeId.from("aws.iam#conditionKeyValue");

public ConditionKeyValueTrait(String conditionKey) {
super(ID, conditionKey, SourceLocation.NONE);
}

public ConditionKeyValueTrait(String conditionKey, FromSourceLocation sourceLocation) {
super(ID, conditionKey, sourceLocation);
}

public static final class Provider extends StringTrait.Provider<ConditionKeyValueTrait> {
public Provider() {
super(ID, ConditionKeyValueTrait::new);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,16 @@
package software.amazon.smithy.aws.iam.traits;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import software.amazon.smithy.aws.traits.ServiceTrait;
import software.amazon.smithy.model.Model;
import software.amazon.smithy.model.knowledge.OperationIndex;
import software.amazon.smithy.model.knowledge.TopDownIndex;
import software.amazon.smithy.model.shapes.MemberShape;
import software.amazon.smithy.model.shapes.OperationShape;
import software.amazon.smithy.model.shapes.ServiceShape;
import software.amazon.smithy.model.validation.AbstractValidator;
Expand All @@ -44,23 +48,67 @@ public final class ConditionKeysValidator extends AbstractValidator {
public List<ValidationEvent> validate(Model model) {
ConditionKeysIndex conditionIndex = ConditionKeysIndex.of(model);
TopDownIndex topDownIndex = TopDownIndex.of(model);
OperationIndex operationIndex = OperationIndex.of(model);

return model.shapes(ServiceShape.class)
.filter(service -> service.hasTrait(ServiceTrait.class))
.flatMap(service -> {
List<ValidationEvent> results = new ArrayList<>();
Set<String> knownKeys = conditionIndex.getDefinedConditionKeys(service).keySet();
Set<String> serviceResolvedKeys = Collections.emptySet();

if (service.hasTrait(ServiceResolvedConditionKeysTrait.class)) {
ServiceResolvedConditionKeysTrait trait =
service.expectTrait(ServiceResolvedConditionKeysTrait.class);
//assign so we can compare against condition key values for any intersection
serviceResolvedKeys = new HashSet<>(trait.getValues());
//copy as this is a destructive action and will affect all future access
List<String> invalidNames = new ArrayList<>(trait.getValues());
invalidNames.removeAll(knownKeys);
if (!invalidNames.isEmpty()) {
results.add(error(service, trait, String.format(
"This condition keys resolved by the `%s` service "
+ "refer to undefined "
+ "condition key(s) [%s]. Expected one of the following "
+ "defined condition keys: [%s]",
service.getId(), ValidationUtils.tickedList(invalidNames),
ValidationUtils.tickedList(knownKeys))));
}
}

for (OperationShape operation : topDownIndex.getContainedOperations(service)) {
for (String name : conditionIndex.getConditionKeyNames(service, operation)) {
if (!knownKeys.contains(name) && !name.startsWith("aws:")) {
results.add(error(operation, String.format(
"This operation scoped within the `%s` service refers to an undefined "
+ "condition key `%s`. Expected one of the following defined condition "
+ "keys: [%s]",
+ "condition key `%s`. Expected one of the following defined condition "
+ "keys: [%s]",
service.getId(), name, ValidationUtils.tickedList(knownKeys))));
}
}

for (MemberShape memberShape : operationIndex.getInputMembers(operation).values()) {
if (memberShape.hasTrait(ConditionKeyValueTrait.class)) {
ConditionKeyValueTrait trait = memberShape.expectTrait(ConditionKeyValueTrait.class);
String conditionKey = trait.getValue();
if (!knownKeys.contains(conditionKey)) {
results.add(error(memberShape, trait, String.format(
"This operation `%s` scoped within the `%s` service with member `%s` "
+ "refers to an undefined "
+ "condition key `%s`. Expected one of the following defined "
+ "condition keys: [%s]",
operation.getId(), service.getId(), memberShape.getId(),
conditionKey, ValidationUtils.tickedList(knownKeys))));
}
if (serviceResolvedKeys.contains(conditionKey)) {
results.add(error(memberShape, trait, String.format(
"This operation `%s` scoped within the `%s` service with member `%s` refers"
+ " to a condition key `%s` that is also resolved by service.",
operation.getId(), service.getId(), memberShape.getId(),
conditionKey, ValidationUtils.tickedList(knownKeys))));
}
}
}
}

return results.stream();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* 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.List;
import software.amazon.smithy.model.FromSourceLocation;
import software.amazon.smithy.model.SourceLocation;
import software.amazon.smithy.model.shapes.ShapeId;
import software.amazon.smithy.model.traits.StringListTrait;

public final class ServiceResolvedConditionKeysTrait extends StringListTrait {
public static final ShapeId ID = ShapeId.from("aws.iam#serviceResolvedConditionKeys");

public ServiceResolvedConditionKeysTrait(List<String> conditionKeys) {
super(ID, conditionKeys, SourceLocation.NONE);
}

public ServiceResolvedConditionKeysTrait(List<String> conditionKeys, FromSourceLocation sourceLocation) {
super(ID, conditionKeys, sourceLocation);
}

public static final class Provider extends StringListTrait.Provider<ServiceResolvedConditionKeysTrait> {
public Provider() {
super(ID, ServiceResolvedConditionKeysTrait::new);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,5 @@ software.amazon.smithy.aws.iam.traits.RequiredActionsTrait$Provider
software.amazon.smithy.aws.iam.traits.SupportedPrincipalTypesTrait$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
software.amazon.smithy.aws.iam.traits.ConditionKeyValueTrait$Provider
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,27 @@
},
"smithy.api#documentation": "Provides a custom IAM action name. By default, the action name is the same as the operation name."
}
},
"aws.iam#serviceResolvedConditionKeys": {
"type": "list",
"member": {
"target": "aws.iam#IamIdentifier"
},
"traits": {
"smithy.api#trait": {
"selector": "service"
},
"smithy.api#documentation": "Specifies the list of IAM condition keys which must be resolved by the service, as opposed to being pulled from the request."
}
},
"aws.iam#conditionKeyValue": {
"type": "string",
"traits": {
"smithy.api#trait": {
"selector": "member"
},
"smithy.api#documentation": "Uses the associated member’s value as this condition key’s value. Needed when the member name doesn't match the condition key name."
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package software.amazon.smithy.aws.iam.traits;

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;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;

public class ConditionKeyValueTraitTest {
@Test
public void loadsFromModel() {
Model result = Model.assembler()
.discoverModels(getClass().getClassLoader())
.addImport(getClass().getResource("condition-key-value.smithy"))
.assemble()
.unwrap();

Shape shape = result.expectShape(ShapeId.from("smithy.example#EchoInput$id1"));
ConditionKeyValueTrait trait = shape.expectTrait(ConditionKeyValueTrait.class);
assertThat(trait.getValue(), equalTo("smithy:ActionContextKey1"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,23 +47,23 @@ public void successfullyLoadsConditionKeys() {
assertThat(index.getConditionKeyNames(service), containsInAnyOrder(
"aws:accountId", "foo:baz", "myservice:Resource1Id1", "myservice:ResourceTwoId2"));
assertThat(index.getConditionKeyNames(service, ShapeId.from("smithy.example#Operation1")),
containsInAnyOrder("aws:accountId", "foo:baz"));
containsInAnyOrder("aws:accountId", "foo:baz"));
assertThat(index.getConditionKeyNames(service, ShapeId.from("smithy.example#Resource1")),
containsInAnyOrder("aws:accountId", "foo:baz", "myservice:Resource1Id1"));
containsInAnyOrder("aws:accountId", "foo:baz", "myservice:Resource1Id1"));
// Note that ID1 is not duplicated but rather reused on the child operation.
assertThat(index.getConditionKeyNames(service, ShapeId.from("smithy.example#Resource2")),
containsInAnyOrder("aws:accountId", "foo:baz",
"myservice:Resource1Id1", "myservice:ResourceTwoId2"));
containsInAnyOrder("aws:accountId", "foo:baz",
"myservice:Resource1Id1", "myservice:ResourceTwoId2"));
// 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()));

// Defined context keys are assembled from the names and mapped with the definitions.
assertThat(index.getDefinedConditionKeys(service).get("myservice:Resource1Id1").getDocumentation(),
not(Optional.empty()));
not(Optional.empty()));
assertEquals(index.getDefinedConditionKeys(service).get("myservice:ResourceTwoId2").getDocumentation().get(),
"This is Foo");
assertThat(index.getDefinedConditionKeys(service, ShapeId.from("smithy.example#GetResource2")).keySet(),
is(empty()));
is(empty()));
}

@Test
Expand All @@ -75,8 +75,8 @@ public void detectsUnknownConditionKeys() {

assertTrue(result.isBroken());
assertThat(result.getValidationEvents(Severity.ERROR).stream()
.map(ValidationEvent::getId)
.collect(Collectors.toSet()),
.map(ValidationEvent::getId)
.collect(Collectors.toSet()),
contains("ConditionKeys"));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package software.amazon.smithy.aws.iam.traits;

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;

import java.util.Collections;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;

public class ServiceResolvedConditionKeysTraitTest {
@Test
public void loadsFromModel() {
Model result = Model.assembler()
.discoverModels(getClass().getClassLoader())
.addImport(getClass().getResource("service-resolved-condition-keys.smithy"))
.assemble()
.unwrap();

Shape shape = result.expectShape(ShapeId.from("smithy.example#MyService"));
ServiceResolvedConditionKeysTrait trait = shape.expectTrait(ServiceResolvedConditionKeysTrait.class);
assertThat(trait.getValues(), equalTo(Collections.singletonList("smithy:ServiceResolveContextKey")));
}
}
Loading

0 comments on commit b5b708a

Please sign in to comment.