Skip to content

Commit 19794e1

Browse files
Add auth documentation
This adds a documentation section to the service's page covering the available auth types and a notice to each operation's page if it has optional auth, fewer auth types, and/or a different auth type.
1 parent 9dacbfb commit 19794e1

File tree

8 files changed

+316
-2
lines changed

8 files changed

+316
-2
lines changed

smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/DocSymbolProvider.java

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,12 @@
2828
import software.amazon.smithy.model.shapes.ShapeVisitor;
2929
import software.amazon.smithy.model.shapes.StructureShape;
3030
import software.amazon.smithy.model.shapes.UnionShape;
31+
import software.amazon.smithy.model.traits.AuthDefinitionTrait;
3132
import software.amazon.smithy.model.traits.InputTrait;
3233
import software.amazon.smithy.model.traits.OutputTrait;
3334
import software.amazon.smithy.model.traits.StringTrait;
3435
import software.amazon.smithy.model.traits.TitleTrait;
36+
import software.amazon.smithy.model.traits.TraitDefinition;
3537
import software.amazon.smithy.utils.SmithyUnstableApi;
3638
import software.amazon.smithy.utils.StringUtils;
3739

@@ -129,6 +131,7 @@ public final class DocSymbolProvider extends ShapeVisitor.Default<Symbol> implem
129131
public static final String ENABLE_DEFAULT_FILE_EXTENSION = "enableDefaultFileExtension";
130132

131133
private static final Logger LOGGER = Logger.getLogger(DocSymbolProvider.class.getName());
134+
private static final String SERVICE_FILE = "index";
132135

133136
private final Model model;
134137
private final DocSettings docSettings;
@@ -177,7 +180,7 @@ public Symbol toSymbol(Shape shape) {
177180
@Override
178181
public Symbol serviceShape(ServiceShape shape) {
179182
return getSymbolBuilder(shape)
180-
.definitionFile(getDefinitionFile("index"))
183+
.definitionFile(getDefinitionFile(SERVICE_FILE))
181184
.build();
182185
}
183186

@@ -193,7 +196,15 @@ public Symbol operationShape(OperationShape shape) {
193196

194197
@Override
195198
public Symbol structureShape(StructureShape shape) {
196-
var builder = getSymbolBuilderWithFile(shape);
199+
var builder = getSymbolBuilder(shape);
200+
if (shape.hasTrait(TraitDefinition.class)) {
201+
if (shape.hasTrait(AuthDefinitionTrait.class)) {
202+
builder.definitionFile(getDefinitionFile(SERVICE_FILE));
203+
}
204+
return builder.build();
205+
}
206+
207+
builder.definitionFile(getDefinitionFile(serviceShape, shape));
197208
if (ioToOperation.containsKey(shape.getId())) {
198209
// Input and output structures are documented on the operation's definition page.
199210
var operation = ioToOperation.get(shape.getId());

smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/DocgenUtils.java

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,17 @@
1414
import java.nio.file.Path;
1515
import java.nio.file.Paths;
1616
import java.util.ArrayList;
17+
import java.util.LinkedHashSet;
1718
import java.util.List;
1819
import java.util.Optional;
1920
import java.util.logging.Logger;
2021
import software.amazon.smithy.codegen.core.CodegenException;
2122
import software.amazon.smithy.codegen.core.Symbol;
23+
import software.amazon.smithy.model.Model;
24+
import software.amazon.smithy.model.knowledge.ServiceIndex;
25+
import software.amazon.smithy.model.knowledge.ServiceIndex.AuthSchemeMode;
26+
import software.amazon.smithy.model.shapes.ShapeId;
27+
import software.amazon.smithy.model.shapes.ToShapeId;
2228
import software.amazon.smithy.utils.SmithyUnstableApi;
2329
import software.amazon.smithy.utils.StringUtils;
2430

@@ -112,4 +118,31 @@ public static Optional<String> getSymbolLink(Symbol symbol, Path relativeTo) {
112118
"./%s#%s", relativeToParent.relativize(Paths.get(symbol.getDefinitionFile())), linkId.get()
113119
));
114120
}
121+
122+
/**
123+
* Gets a priority-ordered list of the service's auth types.
124+
*
125+
* <p>This includes all the auth types bound to the service, not just those present
126+
* in the {@code auth} trait. Auth types not present in the auth trait are at the
127+
* end of the list, in alphabetical order.
128+
*
129+
* @param model The model being generated from.
130+
* @param service The service being documented.
131+
* @return returns a priority-ordered list of service auth types.
132+
*/
133+
public static List<ShapeId> getPrioritizedServiceAuth(Model model, ToShapeId service) {
134+
var index = ServiceIndex.of(model);
135+
136+
// Get the effective auth schemes first and add them to an ordered set. This
137+
// is important to do because the effective schemes are explicitly ordered in
138+
// the model by the auth trait, and we want to document the auth options in
139+
// that same order.
140+
var authSchemes = new LinkedHashSet<>(index.getEffectiveAuthSchemes(service, AuthSchemeMode.MODELED).keySet());
141+
142+
// Since the auth trait can exclude some of the service's auth types, we need
143+
// to add those in last.
144+
authSchemes.addAll(index.getAuthSchemes(service).keySet());
145+
146+
return List.copyOf(authSchemes);
147+
}
115148
}

smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/generators/ServiceGenerator.java

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,17 @@
99
import software.amazon.smithy.codegen.core.directed.GenerateServiceDirective;
1010
import software.amazon.smithy.docgen.core.DocGenerationContext;
1111
import software.amazon.smithy.docgen.core.DocSettings;
12+
import software.amazon.smithy.docgen.core.DocgenUtils;
13+
import software.amazon.smithy.docgen.core.sections.AuthSection;
1214
import software.amazon.smithy.docgen.core.sections.ShapeDetailsSection;
1315
import software.amazon.smithy.docgen.core.sections.ShapeSection;
1416
import software.amazon.smithy.docgen.core.sections.ShapeSubheadingSection;
17+
import software.amazon.smithy.docgen.core.writers.DocWriter;
18+
import software.amazon.smithy.model.knowledge.ServiceIndex;
19+
import software.amazon.smithy.model.knowledge.ServiceIndex.AuthSchemeMode;
1520
import software.amazon.smithy.model.knowledge.TopDownIndex;
21+
import software.amazon.smithy.model.shapes.ServiceShape;
22+
import software.amazon.smithy.model.traits.synthetic.NoAuthTrait;
1623
import software.amazon.smithy.utils.SmithyInternalApi;
1724

1825
/**
@@ -47,6 +54,10 @@
4754
* <li>{@link software.amazon.smithy.docgen.core.sections.BoundResourceSection}:
4855
* enables modifying the listing of an individual resource directly bound to
4956
* the service.
57+
*
58+
* <li>{@link AuthSection} enables modifying the documentation for the different
59+
* auth schemes available on the service. This section will not be present if
60+
* the service has no auth traits.
5061
* </ul>
5162
*
5263
* <p>To change the intermediate format (e.g. from markdown to restructured text),
@@ -87,9 +98,53 @@ public void accept(GenerateServiceDirective<DocGenerationContext, DocSettings> d
8798
var operations = topDownIndex.getContainedOperations(service).stream().sorted().toList();
8899
ServiceShapeGeneratorUtils.generateOperationListing(context, writer, service, operations);
89100

101+
writeAuthSection(context, writer, service);
102+
90103
writer.closeHeading();
91104
writer.popState();
92105
});
93106
}
94107

108+
private void writeAuthSection(DocGenerationContext context, DocWriter writer, ServiceShape service) {
109+
var authSchemes = DocgenUtils.getPrioritizedServiceAuth(context.model(), service);
110+
if (authSchemes.isEmpty()) {
111+
return;
112+
}
113+
114+
writer.pushState(new AuthSection(context, service));
115+
writer.openHeading("Auth");
116+
117+
var index = ServiceIndex.of(context.model());
118+
writer.putContext("optional", index.getEffectiveAuthSchemes(service, AuthSchemeMode.NO_AUTH_AWARE)
119+
.containsKey(NoAuthTrait.ID));
120+
writer.putContext("multipleSchemes", authSchemes.size() > 1);
121+
writer.write("""
122+
Operations on the service ${?optional}may optionally${/optional}${^optional}MUST${/optional} \
123+
be called with ${?multipleSchemes}one of the following priority-ordered auth schemes${/multipleSchemes}\
124+
${^multipleSchemes}the following auth scheme${/multipleSchemes}. Additionally, authentication for \
125+
individual operations may be optional${?multipleSchemes}, have a different priority order, support \
126+
fewer schemes,${/multipleSchemes} or be disabled entirely.
127+
""");
128+
129+
writer.openDefinitionList();
130+
131+
for (var scheme : authSchemes) {
132+
var authTraitShape = context.model().expectShape(scheme);
133+
var authTraitSymbol = context.symbolProvider().toSymbol(authTraitShape);
134+
135+
writer.pushState(new ShapeSection(context, authTraitShape));
136+
writer.openDefinitionListItem(w -> w.write("$R", authTraitSymbol));
137+
138+
writer.injectSection(new ShapeSubheadingSection(context, authTraitShape));
139+
writer.writeShapeDocs(authTraitShape, context.model());
140+
writer.injectSection(new ShapeDetailsSection(context, authTraitShape));
141+
142+
writer.closeDefinitionListItem();
143+
writer.popState();
144+
}
145+
146+
writer.closeDefinitionList();
147+
writer.closeHeading();
148+
writer.popState();
149+
}
95150
}

smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/integrations/BuiltinsIntegration.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import software.amazon.smithy.docgen.core.DocGenerationContext;
1111
import software.amazon.smithy.docgen.core.DocIntegration;
1212
import software.amazon.smithy.docgen.core.DocSettings;
13+
import software.amazon.smithy.docgen.core.interceptors.ApiKeyAuthInterceptor;
1314
import software.amazon.smithy.docgen.core.interceptors.DefaultValueInterceptor;
1415
import software.amazon.smithy.docgen.core.interceptors.DeprecatedInterceptor;
1516
import software.amazon.smithy.docgen.core.interceptors.ErrorFaultInterceptor;
@@ -20,6 +21,7 @@
2021
import software.amazon.smithy.docgen.core.interceptors.NoReplaceBindingInterceptor;
2122
import software.amazon.smithy.docgen.core.interceptors.NoReplaceOperationInterceptor;
2223
import software.amazon.smithy.docgen.core.interceptors.NullabilityInterceptor;
24+
import software.amazon.smithy.docgen.core.interceptors.OperationAuthInterceptor;
2325
import software.amazon.smithy.docgen.core.interceptors.PaginationInterceptor;
2426
import software.amazon.smithy.docgen.core.interceptors.PatternInterceptor;
2527
import software.amazon.smithy.docgen.core.interceptors.RangeInterceptor;
@@ -71,6 +73,8 @@ public List<? extends CodeInterceptor<? extends CodeSection, DocWriter>> interce
7173
// the ones at the end will be at the top of the rendered pages. Therefore, interceptors
7274
// that provide more critical information should appear at the bottom of this list.
7375
return List.of(
76+
new OperationAuthInterceptor(),
77+
new ApiKeyAuthInterceptor(),
7478
new PaginationInterceptor(),
7579
new RequestCompressionInterceptor(),
7680
new NoReplaceBindingInterceptor(),
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
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.docgen.core.interceptors;
7+
8+
import software.amazon.smithy.docgen.core.sections.ShapeDetailsSection;
9+
import software.amazon.smithy.docgen.core.writers.DocWriter;
10+
import software.amazon.smithy.model.traits.HttpApiKeyAuthTrait;
11+
import software.amazon.smithy.model.traits.HttpApiKeyAuthTrait.Location;
12+
import software.amazon.smithy.utils.CodeInterceptor;
13+
import software.amazon.smithy.utils.Pair;
14+
import software.amazon.smithy.utils.SmithyInternalApi;
15+
16+
/**
17+
* Adds additional context to the description of api key auth based on the customized values.
18+
*/
19+
@SmithyInternalApi
20+
public final class ApiKeyAuthInterceptor implements CodeInterceptor<ShapeDetailsSection, DocWriter> {
21+
private static final Pair<String, String> AUTH_HEADER_REF = Pair.of(
22+
"Authorization header", "https://datatracker.ietf.org/doc/html/rfc9110.html#section-11.4"
23+
);
24+
25+
@Override
26+
public Class<ShapeDetailsSection> sectionType() {
27+
return ShapeDetailsSection.class;
28+
}
29+
30+
@Override
31+
public boolean isIntercepted(ShapeDetailsSection section) {
32+
return section.shape().getId().equals(HttpApiKeyAuthTrait.ID);
33+
}
34+
35+
@Override
36+
public void write(DocWriter writer, String previousText, ShapeDetailsSection section) {
37+
var service = section.context().model().expectShape(section.context().settings().service());
38+
var trait = service.expectTrait(HttpApiKeyAuthTrait.class);
39+
writer.putContext("name", trait.getName());
40+
writer.putContext("location", trait.getIn().equals(Location.HEADER) ? "header" : "query string");
41+
writer.putContext("scheme", trait.getScheme());
42+
writer.putContext("authHeader", AUTH_HEADER_REF);
43+
writer.write("""
44+
The API key must be bound to the ${location:L} using the key ${name:`}.${?scheme} \
45+
Additionally, the scheme used in the ${authHeader:R} must be ${scheme:`}.${/scheme}
46+
47+
$L""", previousText);
48+
}
49+
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
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.docgen.core.interceptors;
7+
8+
import java.util.List;
9+
import software.amazon.smithy.docgen.core.DocgenUtils;
10+
import software.amazon.smithy.docgen.core.sections.ShapeDetailsSection;
11+
import software.amazon.smithy.docgen.core.writers.DocWriter;
12+
import software.amazon.smithy.docgen.core.writers.DocWriter.AdmonitionType;
13+
import software.amazon.smithy.model.knowledge.ServiceIndex;
14+
import software.amazon.smithy.model.knowledge.ServiceIndex.AuthSchemeMode;
15+
import software.amazon.smithy.model.shapes.ToShapeId;
16+
import software.amazon.smithy.model.traits.synthetic.NoAuthTrait;
17+
import software.amazon.smithy.utils.CodeInterceptor;
18+
import software.amazon.smithy.utils.SmithyInternalApi;
19+
20+
/**
21+
* Adds a priority list of supported auth schemes for operations with optional auth or
22+
* operations which don't support all of a service's auth schemes.
23+
*/
24+
@SmithyInternalApi
25+
public final class OperationAuthInterceptor implements CodeInterceptor<ShapeDetailsSection, DocWriter> {
26+
@Override
27+
public Class<ShapeDetailsSection> sectionType() {
28+
return ShapeDetailsSection.class;
29+
}
30+
31+
@Override
32+
public boolean isIntercepted(ShapeDetailsSection section) {
33+
if (!section.shape().isOperationShape()) {
34+
return false;
35+
}
36+
var index = ServiceIndex.of(section.context().model());
37+
var service = section.context().settings().service();
38+
39+
// Only add the admonition if the service has auth in the first place.
40+
var serviceAuth = index.getAuthSchemes(service);
41+
if (serviceAuth.isEmpty()) {
42+
return false;
43+
}
44+
45+
// Only add the admonition if the operations' effective auth schemes differs
46+
// from the total list of available auth schemes on the service.
47+
var operationAuth = index.getEffectiveAuthSchemes(service, section.shape(), AuthSchemeMode.NO_AUTH_AWARE);
48+
return !operationAuth.keySet().equals(serviceAuth.keySet());
49+
}
50+
51+
@Override
52+
public void write(DocWriter writer, String previousText, ShapeDetailsSection section) {
53+
writer.writeWithNoFormatting(previousText);
54+
writer.openAdmonition(AdmonitionType.IMPORTANT);
55+
56+
var index = ServiceIndex.of(section.context().model());
57+
var service = section.context().settings().service();
58+
var operation = section.shape();
59+
60+
61+
var serviceAuth = DocgenUtils.getPrioritizedServiceAuth(section.context().model(), service);
62+
var operationAuth = List.copyOf(
63+
index.getEffectiveAuthSchemes(service, operation, AuthSchemeMode.MODELED).keySet());
64+
65+
if (serviceAuth.equals(operationAuth)) {
66+
// If the total service auth and effective *modeled* operation auth are the same,
67+
// that means that the operation just has optional auth since isIntercepted would
68+
// return false otherwise. It would have been overly confusing to include this
69+
// case in the big text block below.
70+
writer.write("""
71+
This operation may be optionally called without authentication.
72+
""");
73+
writer.closeAdmonition();
74+
return;
75+
}
76+
77+
var operationSchemes = operationAuth.stream()
78+
.map(id -> section.context().symbolProvider().toSymbol(section.context().model().expectShape(id)))
79+
.toList();
80+
81+
writer.putContext("optional", supportsNoAuth(index, service, section.shape()));
82+
writer.putContext("schemes", operationSchemes);
83+
writer.putContext("multipleSchemes", operationSchemes.size() > 1);
84+
85+
writer.write("""
86+
${?schemes}This operation ${?optional}may optionally${/optional}${^optional}MUST${/optional} \
87+
be called with ${?multipleSchemes}one of the following priority-ordered auth schemes${/multipleSchemes}\
88+
${^multipleSchemes}the following auth scheme${/multipleSchemes}: \
89+
${#schemes}${value:R}${^key.last}, ${/key.last}${/schemes}.${/schemes}\
90+
${^schemes}${?optional}This operation must be called without authentication.${/optional}${/schemes}
91+
""");
92+
writer.closeAdmonition();
93+
}
94+
95+
private boolean supportsNoAuth(ServiceIndex index, ToShapeId service, ToShapeId operation) {
96+
return index.getEffectiveAuthSchemes(service, operation, AuthSchemeMode.NO_AUTH_AWARE)
97+
.containsKey(NoAuthTrait.ID);
98+
}
99+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
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.docgen.core.sections;
7+
8+
import software.amazon.smithy.docgen.core.DocGenerationContext;
9+
import software.amazon.smithy.model.shapes.ServiceShape;
10+
import software.amazon.smithy.utils.CodeSection;
11+
import software.amazon.smithy.utils.SmithyUnstableApi;
12+
13+
/**
14+
* Contains the documentation for the auth schemes that the service supports.
15+
*
16+
* <p>By default, the auth schemes are documented in a definition list. The title
17+
* used for each auth scheme is the name that results from passing the auth trait's
18+
* shape to the {@link software.amazon.smithy.docgen.core.DocSymbolProvider}. The
19+
* name can be customized by decorating the provider with
20+
* {@link software.amazon.smithy.docgen.core.DocIntegration#decorateSymbolProvider}.
21+
*
22+
* <p>The body of each auth scheme's docs is treated like a typical shape section,
23+
* with a {@link ShapeSection}, {@link ShapeSubheadingSection},
24+
* {@link ShapeDetailsSection}, and documentation pulled from the shape. Details
25+
* based on the trait's values can be inserted via one of those sections, intercepting
26+
* when the shape's id matches the id of the auth trait's shape.
27+
*
28+
* @param context The context used to generate documentation.
29+
* @param service The service whose documentation is being generated.
30+
*
31+
* @see ShapeSection to override documentation for individual auth schemes.
32+
* @see software.amazon.smithy.docgen.core.interceptors.ApiKeyAuthInterceptor for an
33+
* example of adding details to an auth trait's docs based on its values.
34+
*/
35+
@SmithyUnstableApi
36+
public record AuthSection(DocGenerationContext context, ServiceShape service) implements CodeSection {
37+
}

0 commit comments

Comments
 (0)