Skip to content

Commit

Permalink
Adding conditionKeyValue and serviceResolvedConditionKeys traits
Browse files Browse the repository at this point in the history
  • Loading branch information
0xjjoyy committed Aug 17, 2023
1 parent 4dc0d8a commit 4b5c6aa
Show file tree
Hide file tree
Showing 21 changed files with 295 additions and 70 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 @@ -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
Expand Up @@ -11,20 +11,20 @@
import software.amazon.smithy.model.shapes.ShapeId;
import software.amazon.smithy.model.traits.StringListTrait;

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

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

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

public static final class Provider extends StringListTrait.Provider<ConditionKeysResolvedByServiceTrait> {
public static final class Provider extends StringListTrait.Provider<ServiceResolvedConditionKeysTrait> {
public Provider() {
super(ID, ConditionKeysResolvedByServiceTrait::new);
super(ID, ServiceResolvedConditionKeysTrait::new);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +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.ConditionKeysResolvedByServiceTrait$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 @@ -274,7 +274,7 @@
"smithy.api#documentation": "Provides a custom IAM action name. By default, the action name is the same as the operation name."
}
},
"aws.iam#conditionKeysResolvedByService": {
"aws.iam#serviceResolvedConditionKeys": {
"type": "list",
"member": {
"target": "aws.iam#IamIdentifier"
Expand All @@ -288,9 +288,6 @@
},
"aws.iam#conditionKeyValue": {
"type": "string",
"member": {
"target": "aws.iam#IamIdentifier"
},
"traits": {
"smithy.api#trait": {
"selector": "member"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ public void loadsFromModel() {
.assemble()
.unwrap();

Shape shape = result.expectShape(ShapeId.from("smithy.example#GetResource2Input$id1"));
Shape shape = result.expectShape(ShapeId.from("smithy.example#EchoInput$id1"));
ConditionKeyValueTrait trait = shape.expectTrait(ConditionKeyValueTrait.class);
assertThat(trait.getValue(), equalTo("smithy:ActionContextKey2"));
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
Expand Up @@ -5,13 +5,12 @@
import software.amazon.smithy.model.shapes.Shape;
import software.amazon.smithy.model.shapes.ShapeId;

import java.util.Arrays;
import java.util.Collections;

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

public class ConditionKeysResolvedByServiceTraitTest {
public class ServiceResolvedConditionKeysTraitTest {
@Test
public void loadsFromModel() {
Model result = Model.assembler()
Expand All @@ -21,7 +20,7 @@ public void loadsFromModel() {
.unwrap();

Shape shape = result.expectShape(ShapeId.from("smithy.example#MyService"));
ConditionKeysResolvedByServiceTrait trait = shape.expectTrait(ConditionKeysResolvedByServiceTrait.class);
assertThat(trait.getValues(), equalTo(Collections.singletonList("smithy:requesterId")));
ServiceResolvedConditionKeysTrait trait = shape.expectTrait(ServiceResolvedConditionKeysTrait.class);
assertThat(trait.getValues(), equalTo(Collections.singletonList("smithy:ServiceResolveContextKey")));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
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<SmithyTestCase.Result> callable) throws Exception {
callable.call();
}

public static Stream<?> source() {
return SmithyTestSuite.defaultParameterizedTestSource(SmithyErrorFilesTest.class);
}
}
Original file line number Diff line number Diff line change
@@ -1,33 +1,20 @@
$version: "2.0"
namespace smithy.example

use aws.iam#conditionKeyValue
namespace smithy.example

@aws.iam#defineConditionKeys(
"smithy:ActionContextKey1": { type: "String" },
"smithy:ActionContextKey2": { type: "String" },
"smithy:ActionContextKey3": { type: "String" },
"smithy:requesterId": { type: "String" }
"smithy:ActionContextKey1": { type: "String" }
)
@aws.iam#conditionKeysResolvedByService(["smithy:requesterId"])
@aws.auth#sigv4(name: "smithy")
service MyService {
version: "2019-02-20",
operations: [Echo, GetResource2]
operations: [Echo]
}


@aws.iam#actionName("overridingActionName")
operation Echo {}

operation GetResource2 {
input: GetResource2Input
operation Echo {
input: EchoInput
}

structure GetResource2Input {
@aws.iam#conditionKeyValue("smithy:ActionContextKey2")
id1: String,

@required
id2: String
structure EchoInput {
@aws.iam#conditionKeyValue("smithy:ActionContextKey1")
id1: String
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[ERROR] smithy.example#EchoInput$id1: This operation `smithy.example#Echo` scoped within the `smithy.example#MyService` service with member `smithy.example#EchoInput$id1` refers to a condition key `smithy:ServiceResolveContextKey` that is also resolved by service. | ConditionKeys
Loading

0 comments on commit 4b5c6aa

Please sign in to comment.