Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding conditionKeyValue and conditionKeysResolvedByService traits #1677

Merged
merged 2 commits into from
Aug 18, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
@@ -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");

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

private 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");

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

private 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"));
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A lot of content changed in this file that wasn't actually the specific test additions.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated this file and applied only the new tests.

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")));
}
}
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);
}
}
Loading