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

Add required for service resolved condition keys #2288

Merged
merged 3 commits into from
Jun 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
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
14 changes: 13 additions & 1 deletion docs/source-2.0/aws/aws-iam.rst
Original file line number Diff line number Diff line change
Expand Up @@ -432,6 +432,11 @@ Each condition key structure supports the following members:
- ``string``
- A relative URL path that defines more information about the condition key
within a set of IAM-related documentation.
* - required
- ``boolean``
- Defines whether a service resolved condition key is required. Not
applicable to request resolved condition keys, as the native
:ref:`required-trait` trait MUST be used on the member directly.

.. code-block:: smithy

Expand All @@ -448,7 +453,14 @@ Each condition key structure supports the following members:
type: "String"
documentation: "The Bar string"
externalDocumentation: "http://example.com"
})
},
"myservice:Baz": {
type: "String"
documentation: "The Baz string"
externalDocumentation: "http://baz.com"
required: true
}
)
service MyService {
version: "2017-02-11"
resources: [MyResource]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

import java.util.Objects;
import java.util.Optional;
import software.amazon.smithy.model.node.BooleanNode;
import software.amazon.smithy.model.node.Node;
import software.amazon.smithy.model.node.ObjectNode;
import software.amazon.smithy.model.node.StringNode;
Expand All @@ -29,17 +30,20 @@ public final class ConditionKeyDefinition implements ToNode, ToSmithyBuilder<Con
private static final String DOCUMENTATION = "documentation";
private static final String EXTERNAL_DOCUMENTATION = "externalDocumentation";
private static final String RELATIVE_DOCUMENTATION = "relativeDocumentation";
private static final String REQUIRED = "required";

private final String type;
private final String documentation;
private final String externalDocumentation;
private final String relativeDocumentation;
private final boolean required;

private ConditionKeyDefinition(Builder builder) {
type = SmithyBuilder.requiredState(TYPE, builder.type);
documentation = builder.documentation;
externalDocumentation = builder.externalDocumentation;
relativeDocumentation = builder.relativeDocumentation;
required = builder.required;
}

public static Builder builder() {
Expand All @@ -56,6 +60,8 @@ public static ConditionKeyDefinition fromNode(Node value) {
.ifPresent(builder::externalDocumentation);
objectNode.getStringMember(RELATIVE_DOCUMENTATION).map(StringNode::getValue)
.ifPresent(builder::relativeDocumentation);
objectNode.getBooleanMember(REQUIRED).map(BooleanNode::getValue)
.ifPresent(builder::required);

return builder.build();
}
Expand Down Expand Up @@ -91,13 +97,24 @@ public Optional<String> getRelativeDocumentation() {
return Optional.ofNullable(relativeDocumentation);
}

/**
* Whether a service resolved condition key is required. Not applicable to request resolved condition keys,
* as the native @required trait must be used.
*
* @return If the service resolved condition key is required.
**/
public boolean isRequired() {
return required;
}

@Override
public SmithyBuilder<ConditionKeyDefinition> toBuilder() {
return builder()
.documentation(documentation)
.externalDocumentation(externalDocumentation)
.relativeDocumentation(relativeDocumentation)
.type(type);
.type(type)
.required(required);
}

@Override
Expand All @@ -107,6 +124,7 @@ public Node toNode() {
.withOptionalMember(DOCUMENTATION, getDocumentation().map(Node::from))
.withOptionalMember(EXTERNAL_DOCUMENTATION, getExternalDocumentation().map(Node::from))
.withOptionalMember(RELATIVE_DOCUMENTATION, getRelativeDocumentation().map(Node::from))
.withMember(REQUIRED, isRequired())
.build();
}

Expand All @@ -122,7 +140,8 @@ public boolean equals(Object o) {
return Objects.equals(type, that.type)
&& Objects.equals(documentation, that.documentation)
&& Objects.equals(externalDocumentation, that.externalDocumentation)
&& Objects.equals(relativeDocumentation, that.relativeDocumentation);
&& Objects.equals(relativeDocumentation, that.relativeDocumentation)
&& Objects.equals(required, that.required);
}

@Override
Expand All @@ -135,6 +154,7 @@ public static final class Builder implements SmithyBuilder<ConditionKeyDefinitio
private String documentation;
private String externalDocumentation;
private String relativeDocumentation;
private boolean required;

@Override
public ConditionKeyDefinition build() {
Expand All @@ -160,5 +180,10 @@ public Builder relativeDocumentation(String relativeDocumentation) {
this.relativeDocumentation = relativeDocumentation;
return this;
}

public Builder required(boolean required) {
this.required = required;
return this;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
* 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.List;
import java.util.Map;
import software.amazon.smithy.model.FromSourceLocation;
import software.amazon.smithy.model.Model;
import software.amazon.smithy.model.shapes.ServiceShape;
import software.amazon.smithy.model.traits.StringListTrait;
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.ListUtils;

/**
* Checks properties of the defineConditionKeys IAM trait
* 1. @required only for service resolved condition keys. ERROR if used for request resolved condition keys.
*/
public final class DefineConditionKeysTraitValidator extends AbstractValidator {
@Override
public List<ValidationEvent> validate(Model model) {
List<ValidationEvent> events = new ArrayList<>();
ConditionKeysIndex conditionKeysIndex = ConditionKeysIndex.of(model);
for (ServiceShape serviceShape : model.getServiceShapes()) {
for (Map.Entry<String, ConditionKeyDefinition> conditionKeyEntry
: conditionKeysIndex.getDefinedConditionKeys(serviceShape).entrySet()
) {
// Get all the service resolved condition keys.
List<String> serviceResolvedKeys = serviceShape.getTrait(ServiceResolvedConditionKeysTrait.class)
.map(StringListTrait::getValues)
.orElse(ListUtils.of());
// We want to emit an event for definitions that state being required...
if (conditionKeyEntry.getValue().isRequired()
// and are not configured to be service resolved.
&& !serviceResolvedKeys.contains(conditionKeyEntry.getKey())
) {
FromSourceLocation sourceLocation = serviceShape.getTrait(DefineConditionKeysTrait.class)
.map(Trait::getSourceLocation)
.orElse(serviceShape.getSourceLocation());
events.add(error(serviceShape, sourceLocation, format("The `%s` condition key is defined as "
+ "required but is resolved from the request. This property is only valid for condition "
+ "keys resolved by the service, it MUST also be specified in the "
+ "`@serviceResolvedConditionKeys` trait or use the `@required` trait instead.",
conditionKeyEntry.getKey())));
}
}
}
return events;
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
software.amazon.smithy.aws.iam.traits.ConditionKeysValidator
software.amazon.smithy.aws.iam.traits.IamActionValidator
software.amazon.smithy.aws.iam.traits.IamResourceTraitValidator
software.amazon.smithy.aws.iam.traits.DefineConditionKeysTraitValidator
software.amazon.smithy.aws.iam.traits.IamResourceTraitConflictingNameValidator
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,10 @@ structure ConditionKeyDefinition {
/// A relative URL path that defines more information about the condition key
/// within a set of IAM-related documentation.
relativeDocumentation: String

/// Whether a service resolved condition key is required.
/// Request resolved condition keys MUST use the @required trait.
required: Boolean
}

/// Contains information about a resource an IAM action can be authorized against.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
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.contains;
import static org.hamcrest.Matchers.equalTo;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.assertFalse;


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


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

Shape shape = result.expectShape(ShapeId.from("smithy.example#MyService"));
DefineConditionKeysTrait trait = shape.expectTrait(DefineConditionKeysTrait.class);
assertEquals(3,trait.getConditionKeys().size());
assertFalse(trait.getConditionKey("myservice:Bar").get().isRequired());
assertFalse(trait.getConditionKey("myservice:Foo").get().isRequired());
assertTrue(trait.getConditionKey("myservice:Baz").get().isRequired());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
$version: "2"

namespace smithy.example

use aws.api#service
use aws.iam#defineConditionKeys
use aws.iam#serviceResolvedConditionKeys

@service(sdkId: "My Value", arnNamespace: "myservice")
@defineConditionKeys(
"myservice:Bar": {
type: "String"
documentation: "The Bar string"
externalDocumentation: "http://example.com"
},
"myservice:Baz": {
type: "String"
documentation: "The Baz string"
externalDocumentation: "http://baz.com"
required: true
}
"myservice:Foo": {
type: "String"
documentation: "The Foo string"
externalDocumentation: "http://foo.com"
required: false
}
)
@serviceResolvedConditionKeys(["myservice:Baz"])
service MyService {
version: "2017-02-11"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
$version: "2"

namespace smithy.example

use aws.api#service
use aws.iam#defineConditionKeys

@service(sdkId: "My Value", arnNamespace: "myservice")
@defineConditionKeys(
"myservice:Bar": {
type: "String"
documentation: "The Bar string"
externalDocumentation: "http://example.com"
})
service MyService {
version: "2017-02-11"
}

Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[ERROR] smithy.example#MyService: The `myservice:Bar` condition key is defined as required but is resolved from the request. This property is only valid for condition keys resolved by the service, it MUST also be specified in the `@serviceResolvedConditionKeys` trait or use the `@required` trait instead. | DefineConditionKeysTrait
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
$version: "2"

namespace smithy.example

use aws.api#service
use aws.iam#defineConditionKeys

@service(sdkId: "My Value", arnNamespace: "myservice")
@defineConditionKeys(
"myservice:Bar": {
type: "String"
documentation: "The Bar string"
externalDocumentation: "http://example.com"
required: true
})
service MyService {
version: "2017-02-11"
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
$version: "2"

namespace smithy.example

use aws.api#service
use aws.iam#defineConditionKeys
use aws.iam#serviceResolvedConditionKeys

@service(sdkId: "My Value", arnNamespace: "myservice")
@defineConditionKeys(
"myservice:Bar": {
type: "String"
documentation: "The Bar string"
externalDocumentation: "http://example.com"
required: true
})
@serviceResolvedConditionKeys(["myservice:Bar"])
service MyService {
version: "2017-02-11"
}

Loading