Skip to content

Commit b5b708a

Browse files
authored
Add conditionKeyValue and serviceResolvedConditionKeys (#1677)
* Adding conditionKeyValue and serviceResolvedConditionKeys traits * Update trait constructors to be public
1 parent 7bf6e00 commit b5b708a

24 files changed

+441
-13
lines changed

docs/source-2.0/aws/aws-iam.rst

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -434,6 +434,96 @@ The following example defines two operations:
434434
@actionName("OverridingActionName")
435435
operation OperationB {}
436436
437+
.. smithy-trait:: aws.iam#serviceResolvedConditionKeys
438+
.. _aws.iam#serviceResolvedConditionKeys-trait:
439+
440+
----------------------------------------------
441+
``aws.iam#serviceResolvedConditionKeys`` trait
442+
----------------------------------------------
443+
444+
Summary
445+
Specifies the list of IAM condition keys which must be resolved by the
446+
service, as opposed to the value being pulled from the request.
447+
Trait selector
448+
``service``
449+
Value type
450+
``list<string>``
451+
452+
All condition keys defined with the ``serviceResolvedConditionKeys`` trait
453+
MUST also be defined via the :ref:`aws.iam#defineConditionKeys-trait` trait.
454+
Derived resource condition keys MUST NOT be included
455+
with the ``serviceResolvedConditionKeys`` trait.
456+
457+
The following example defines two service-specific condition keys:
458+
459+
* ``myservice:ActionContextKey1`` is expected to be resolved by the service.
460+
* ``myservice:ActionContextKey2`` is expected to be pulled from the request.
461+
462+
.. code-block:: smithy
463+
464+
$version: "2"
465+
466+
namespace smithy.example
467+
468+
@defineConditionKeys(
469+
"myservice:ActionContextKey1": { type: "String" },
470+
"myservice:ActionContextKey2": { type: "String" }
471+
)
472+
@serviceResolvedConditionKeys(["myservice:ActionContextKey1"])
473+
@service(sdkId: "My Value", arnNamespace: "myservice")
474+
service MyService {
475+
version: "2018-05-10"
476+
}
477+
478+
479+
.. smithy-trait:: aws.iam#conditionKeyValue
480+
.. _aws.iam#conditionKeyValue-trait:
481+
482+
-----------------------------------
483+
``aws.iam#conditionKeyValue`` trait
484+
-----------------------------------
485+
486+
Summary
487+
Uses the associated member’s value for the specified condition key.
488+
Trait selector
489+
``member``
490+
Value type
491+
``string``
492+
493+
Members not annotated with the ``conditionKeyValue`` trait, default to the
494+
:ref:`shape name of the shape ID <shape-id>` of the targeted member. All
495+
condition keys defined with the ``conditionKeyValue`` trait MUST also be
496+
defined via the :ref:`aws.iam#defineConditionKeys-trait` trait.
497+
498+
In the input shape for ``OperationA``, the trait ``conditionKeyValue``
499+
explicitly binds ``ActionContextKey1`` to the field ``key``.
500+
501+
.. code-block:: smithy
502+
503+
$version: "2"
504+
505+
namespace smithy.example
506+
507+
@defineConditionKeys(
508+
"myservice:ActionContextKey1": { type: "String" }
509+
)
510+
@service(sdkId: "My Value", arnNamespace: "myservice")
511+
service MyService {
512+
version: "2020-07-02"
513+
operations: [OperationA]
514+
}
515+
516+
@conditionKeys(["myservice:ActionContextKey1"])
517+
operation OperationA {
518+
input := {
519+
@conditionKeyValue("ActionContextKey1")
520+
key: String
521+
}
522+
output := {
523+
out: String
524+
}
525+
}
526+
437527
438528
.. _deriving-condition-keys:
439529

smithy-aws-iam-traits/src/main/java/software/amazon/smithy/aws/iam/traits/ActionNameTrait.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,11 @@ public final class ActionNameTrait extends StringTrait {
1414

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

17-
private ActionNameTrait(String action) {
17+
public ActionNameTrait(String action) {
1818
super(ID, action, SourceLocation.NONE);
1919
}
2020

21-
private ActionNameTrait(String action, FromSourceLocation sourceLocation) {
21+
public ActionNameTrait(String action, FromSourceLocation sourceLocation) {
2222
super(ID, action, sourceLocation);
2323
}
2424

smithy-aws-iam-traits/src/main/java/software/amazon/smithy/aws/iam/traits/ActionPermissionDescriptionTrait.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,11 @@
2525
public final class ActionPermissionDescriptionTrait extends StringTrait {
2626
public static final ShapeId ID = ShapeId.from("aws.iam#actionPermissionDescription");
2727

28-
private ActionPermissionDescriptionTrait(String value, SourceLocation sourceLocation) {
28+
public ActionPermissionDescriptionTrait(String value) {
29+
super(ID, value, SourceLocation.NONE);
30+
}
31+
32+
public ActionPermissionDescriptionTrait(String value, SourceLocation sourceLocation) {
2933
super(ID, value, sourceLocation);
3034
}
3135

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package software.amazon.smithy.aws.iam.traits;
7+
8+
import software.amazon.smithy.model.FromSourceLocation;
9+
import software.amazon.smithy.model.SourceLocation;
10+
import software.amazon.smithy.model.shapes.ShapeId;
11+
import software.amazon.smithy.model.traits.StringTrait;
12+
13+
public final class ConditionKeyValueTrait extends StringTrait {
14+
public static final ShapeId ID = ShapeId.from("aws.iam#conditionKeyValue");
15+
16+
public ConditionKeyValueTrait(String conditionKey) {
17+
super(ID, conditionKey, SourceLocation.NONE);
18+
}
19+
20+
public ConditionKeyValueTrait(String conditionKey, FromSourceLocation sourceLocation) {
21+
super(ID, conditionKey, sourceLocation);
22+
}
23+
24+
public static final class Provider extends StringTrait.Provider<ConditionKeyValueTrait> {
25+
public Provider() {
26+
super(ID, ConditionKeyValueTrait::new);
27+
}
28+
}
29+
}

smithy-aws-iam-traits/src/main/java/software/amazon/smithy/aws/iam/traits/ConditionKeysValidator.java

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,16 @@
1616
package software.amazon.smithy.aws.iam.traits;
1717

1818
import java.util.ArrayList;
19+
import java.util.Collections;
20+
import java.util.HashSet;
1921
import java.util.List;
2022
import java.util.Set;
2123
import java.util.stream.Collectors;
2224
import software.amazon.smithy.aws.traits.ServiceTrait;
2325
import software.amazon.smithy.model.Model;
26+
import software.amazon.smithy.model.knowledge.OperationIndex;
2427
import software.amazon.smithy.model.knowledge.TopDownIndex;
28+
import software.amazon.smithy.model.shapes.MemberShape;
2529
import software.amazon.smithy.model.shapes.OperationShape;
2630
import software.amazon.smithy.model.shapes.ServiceShape;
2731
import software.amazon.smithy.model.validation.AbstractValidator;
@@ -44,23 +48,67 @@ public final class ConditionKeysValidator extends AbstractValidator {
4448
public List<ValidationEvent> validate(Model model) {
4549
ConditionKeysIndex conditionIndex = ConditionKeysIndex.of(model);
4650
TopDownIndex topDownIndex = TopDownIndex.of(model);
51+
OperationIndex operationIndex = OperationIndex.of(model);
4752

4853
return model.shapes(ServiceShape.class)
4954
.filter(service -> service.hasTrait(ServiceTrait.class))
5055
.flatMap(service -> {
5156
List<ValidationEvent> results = new ArrayList<>();
5257
Set<String> knownKeys = conditionIndex.getDefinedConditionKeys(service).keySet();
58+
Set<String> serviceResolvedKeys = Collections.emptySet();
59+
60+
if (service.hasTrait(ServiceResolvedConditionKeysTrait.class)) {
61+
ServiceResolvedConditionKeysTrait trait =
62+
service.expectTrait(ServiceResolvedConditionKeysTrait.class);
63+
//assign so we can compare against condition key values for any intersection
64+
serviceResolvedKeys = new HashSet<>(trait.getValues());
65+
//copy as this is a destructive action and will affect all future access
66+
List<String> invalidNames = new ArrayList<>(trait.getValues());
67+
invalidNames.removeAll(knownKeys);
68+
if (!invalidNames.isEmpty()) {
69+
results.add(error(service, trait, String.format(
70+
"This condition keys resolved by the `%s` service "
71+
+ "refer to undefined "
72+
+ "condition key(s) [%s]. Expected one of the following "
73+
+ "defined condition keys: [%s]",
74+
service.getId(), ValidationUtils.tickedList(invalidNames),
75+
ValidationUtils.tickedList(knownKeys))));
76+
}
77+
}
5378

5479
for (OperationShape operation : topDownIndex.getContainedOperations(service)) {
5580
for (String name : conditionIndex.getConditionKeyNames(service, operation)) {
5681
if (!knownKeys.contains(name) && !name.startsWith("aws:")) {
5782
results.add(error(operation, String.format(
5883
"This operation scoped within the `%s` service refers to an undefined "
59-
+ "condition key `%s`. Expected one of the following defined condition "
60-
+ "keys: [%s]",
84+
+ "condition key `%s`. Expected one of the following defined condition "
85+
+ "keys: [%s]",
6186
service.getId(), name, ValidationUtils.tickedList(knownKeys))));
6287
}
6388
}
89+
90+
for (MemberShape memberShape : operationIndex.getInputMembers(operation).values()) {
91+
if (memberShape.hasTrait(ConditionKeyValueTrait.class)) {
92+
ConditionKeyValueTrait trait = memberShape.expectTrait(ConditionKeyValueTrait.class);
93+
String conditionKey = trait.getValue();
94+
if (!knownKeys.contains(conditionKey)) {
95+
results.add(error(memberShape, trait, String.format(
96+
"This operation `%s` scoped within the `%s` service with member `%s` "
97+
+ "refers to an undefined "
98+
+ "condition key `%s`. Expected one of the following defined "
99+
+ "condition keys: [%s]",
100+
operation.getId(), service.getId(), memberShape.getId(),
101+
conditionKey, ValidationUtils.tickedList(knownKeys))));
102+
}
103+
if (serviceResolvedKeys.contains(conditionKey)) {
104+
results.add(error(memberShape, trait, String.format(
105+
"This operation `%s` scoped within the `%s` service with member `%s` refers"
106+
+ " to a condition key `%s` that is also resolved by service.",
107+
operation.getId(), service.getId(), memberShape.getId(),
108+
conditionKey, ValidationUtils.tickedList(knownKeys))));
109+
}
110+
}
111+
}
64112
}
65113

66114
return results.stream();
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package software.amazon.smithy.aws.iam.traits;
7+
8+
import java.util.List;
9+
import software.amazon.smithy.model.FromSourceLocation;
10+
import software.amazon.smithy.model.SourceLocation;
11+
import software.amazon.smithy.model.shapes.ShapeId;
12+
import software.amazon.smithy.model.traits.StringListTrait;
13+
14+
public final class ServiceResolvedConditionKeysTrait extends StringListTrait {
15+
public static final ShapeId ID = ShapeId.from("aws.iam#serviceResolvedConditionKeys");
16+
17+
public ServiceResolvedConditionKeysTrait(List<String> conditionKeys) {
18+
super(ID, conditionKeys, SourceLocation.NONE);
19+
}
20+
21+
public ServiceResolvedConditionKeysTrait(List<String> conditionKeys, FromSourceLocation sourceLocation) {
22+
super(ID, conditionKeys, sourceLocation);
23+
}
24+
25+
public static final class Provider extends StringListTrait.Provider<ServiceResolvedConditionKeysTrait> {
26+
public Provider() {
27+
super(ID, ServiceResolvedConditionKeysTrait::new);
28+
}
29+
}
30+
}

smithy-aws-iam-traits/src/main/resources/META-INF/services/software.amazon.smithy.model.traits.TraitService

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,5 @@ software.amazon.smithy.aws.iam.traits.RequiredActionsTrait$Provider
66
software.amazon.smithy.aws.iam.traits.SupportedPrincipalTypesTrait$Provider
77
software.amazon.smithy.aws.iam.traits.IamResourceTrait$Provider
88
software.amazon.smithy.aws.iam.traits.ActionNameTrait$Provider
9+
software.amazon.smithy.aws.iam.traits.ServiceResolvedConditionKeysTrait$Provider
10+
software.amazon.smithy.aws.iam.traits.ConditionKeyValueTrait$Provider

smithy-aws-iam-traits/src/main/resources/META-INF/smithy/aws.iam.json

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,27 @@
273273
},
274274
"smithy.api#documentation": "Provides a custom IAM action name. By default, the action name is the same as the operation name."
275275
}
276+
},
277+
"aws.iam#serviceResolvedConditionKeys": {
278+
"type": "list",
279+
"member": {
280+
"target": "aws.iam#IamIdentifier"
281+
},
282+
"traits": {
283+
"smithy.api#trait": {
284+
"selector": "service"
285+
},
286+
"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."
287+
}
288+
},
289+
"aws.iam#conditionKeyValue": {
290+
"type": "string",
291+
"traits": {
292+
"smithy.api#trait": {
293+
"selector": "member"
294+
},
295+
"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."
296+
}
276297
}
277298
}
278299
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package software.amazon.smithy.aws.iam.traits;
2+
3+
import org.junit.jupiter.api.Test;
4+
import software.amazon.smithy.model.Model;
5+
import software.amazon.smithy.model.shapes.Shape;
6+
import software.amazon.smithy.model.shapes.ShapeId;
7+
8+
import static org.hamcrest.MatcherAssert.assertThat;
9+
import static org.hamcrest.Matchers.equalTo;
10+
11+
public class ConditionKeyValueTraitTest {
12+
@Test
13+
public void loadsFromModel() {
14+
Model result = Model.assembler()
15+
.discoverModels(getClass().getClassLoader())
16+
.addImport(getClass().getResource("condition-key-value.smithy"))
17+
.assemble()
18+
.unwrap();
19+
20+
Shape shape = result.expectShape(ShapeId.from("smithy.example#EchoInput$id1"));
21+
ConditionKeyValueTrait trait = shape.expectTrait(ConditionKeyValueTrait.class);
22+
assertThat(trait.getValue(), equalTo("smithy:ActionContextKey1"));
23+
}
24+
}

smithy-aws-iam-traits/src/test/java/software/amazon/smithy/aws/iam/traits/ConditionKeysIndexTest.java

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -47,23 +47,23 @@ public void successfullyLoadsConditionKeys() {
4747
assertThat(index.getConditionKeyNames(service), containsInAnyOrder(
4848
"aws:accountId", "foo:baz", "myservice:Resource1Id1", "myservice:ResourceTwoId2"));
4949
assertThat(index.getConditionKeyNames(service, ShapeId.from("smithy.example#Operation1")),
50-
containsInAnyOrder("aws:accountId", "foo:baz"));
50+
containsInAnyOrder("aws:accountId", "foo:baz"));
5151
assertThat(index.getConditionKeyNames(service, ShapeId.from("smithy.example#Resource1")),
52-
containsInAnyOrder("aws:accountId", "foo:baz", "myservice:Resource1Id1"));
52+
containsInAnyOrder("aws:accountId", "foo:baz", "myservice:Resource1Id1"));
5353
// Note that ID1 is not duplicated but rather reused on the child operation.
5454
assertThat(index.getConditionKeyNames(service, ShapeId.from("smithy.example#Resource2")),
55-
containsInAnyOrder("aws:accountId", "foo:baz",
56-
"myservice:Resource1Id1", "myservice:ResourceTwoId2"));
55+
containsInAnyOrder("aws:accountId", "foo:baz",
56+
"myservice:Resource1Id1", "myservice:ResourceTwoId2"));
5757
// Note that while this operation binds identifiers, it contains no unique ConditionKeys to bind.
5858
assertThat(index.getConditionKeyNames(service, ShapeId.from("smithy.example#GetResource2")), is(empty()));
5959

6060
// Defined context keys are assembled from the names and mapped with the definitions.
6161
assertThat(index.getDefinedConditionKeys(service).get("myservice:Resource1Id1").getDocumentation(),
62-
not(Optional.empty()));
62+
not(Optional.empty()));
6363
assertEquals(index.getDefinedConditionKeys(service).get("myservice:ResourceTwoId2").getDocumentation().get(),
6464
"This is Foo");
6565
assertThat(index.getDefinedConditionKeys(service, ShapeId.from("smithy.example#GetResource2")).keySet(),
66-
is(empty()));
66+
is(empty()));
6767
}
6868

6969
@Test
@@ -75,8 +75,8 @@ public void detectsUnknownConditionKeys() {
7575

7676
assertTrue(result.isBroken());
7777
assertThat(result.getValidationEvents(Severity.ERROR).stream()
78-
.map(ValidationEvent::getId)
79-
.collect(Collectors.toSet()),
78+
.map(ValidationEvent::getId)
79+
.collect(Collectors.toSet()),
8080
contains("ConditionKeys"));
8181
}
8282
}

0 commit comments

Comments
 (0)