Skip to content

Commit

Permalink
Add auth documentation
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
JordonPhillips committed Nov 24, 2023
1 parent 9dacbfb commit 06ef518
Show file tree
Hide file tree
Showing 8 changed files with 304 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,12 @@
import software.amazon.smithy.model.shapes.ShapeVisitor;
import software.amazon.smithy.model.shapes.StructureShape;
import software.amazon.smithy.model.shapes.UnionShape;
import software.amazon.smithy.model.traits.AuthDefinitionTrait;
import software.amazon.smithy.model.traits.InputTrait;
import software.amazon.smithy.model.traits.OutputTrait;
import software.amazon.smithy.model.traits.StringTrait;
import software.amazon.smithy.model.traits.TitleTrait;
import software.amazon.smithy.model.traits.TraitDefinition;
import software.amazon.smithy.utils.SmithyUnstableApi;
import software.amazon.smithy.utils.StringUtils;

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

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

private final Model model;
private final DocSettings docSettings;
Expand Down Expand Up @@ -177,7 +180,7 @@ public Symbol toSymbol(Shape shape) {
@Override
public Symbol serviceShape(ServiceShape shape) {
return getSymbolBuilder(shape)
.definitionFile(getDefinitionFile("index"))
.definitionFile(getDefinitionFile(SERVICE_FILE))
.build();
}

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

@Override
public Symbol structureShape(StructureShape shape) {
var builder = getSymbolBuilderWithFile(shape);
var builder = getSymbolBuilder(shape);
if (shape.hasTrait(TraitDefinition.class)) {
if (shape.hasTrait(AuthDefinitionTrait.class)) {
builder.definitionFile(getDefinitionFile(SERVICE_FILE));
}
return builder.build();
}

builder.definitionFile(getDefinitionFile(serviceShape, shape));
if (ioToOperation.containsKey(shape.getId())) {
// Input and output structures are documented on the operation's definition page.
var operation = ioToOperation.get(shape.getId());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,17 @@
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Optional;
import java.util.logging.Logger;
import software.amazon.smithy.codegen.core.CodegenException;
import software.amazon.smithy.codegen.core.Symbol;
import software.amazon.smithy.model.Model;
import software.amazon.smithy.model.knowledge.ServiceIndex;
import software.amazon.smithy.model.knowledge.ServiceIndex.AuthSchemeMode;
import software.amazon.smithy.model.shapes.ShapeId;
import software.amazon.smithy.model.shapes.ToShapeId;
import software.amazon.smithy.utils.SmithyUnstableApi;
import software.amazon.smithy.utils.StringUtils;

Expand Down Expand Up @@ -112,4 +118,31 @@ public static Optional<String> getSymbolLink(Symbol symbol, Path relativeTo) {
"./%s#%s", relativeToParent.relativize(Paths.get(symbol.getDefinitionFile())), linkId.get()
));
}

/**
* Gets a priority-ordered list of the service's auth types.
*
* <p>This includes all the auth types bound to the service, not just those present
* in the {@code auth} trait. Auth types not present in the auth trait are at the
* end of the list, in alphabetical order.
*
* @param model The model being generated from.
* @param service The service being documented.
* @return returns a priority-ordered list of service auth types.
*/
public static List<ShapeId> getPrioritizedServiceAuth(Model model, ToShapeId service) {
var index = ServiceIndex.of(model);

// Get the effective auth schemes first and add them to an ordered set. This
// is important to do because the effective schemes are explicitly ordered in
// the model by the auth trait, and we want to document the auth options in
// that same order.
var authSchemes = new LinkedHashSet<>(index.getEffectiveAuthSchemes(service, AuthSchemeMode.MODELED).keySet());

// Since the auth trait can exclude some of the service's auth types, we need
// to add those in last.
authSchemes.addAll(index.getAuthSchemes(service).keySet());

return List.copyOf(authSchemes);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,17 @@
import software.amazon.smithy.codegen.core.directed.GenerateServiceDirective;
import software.amazon.smithy.docgen.core.DocGenerationContext;
import software.amazon.smithy.docgen.core.DocSettings;
import software.amazon.smithy.docgen.core.DocgenUtils;
import software.amazon.smithy.docgen.core.sections.AuthSection;
import software.amazon.smithy.docgen.core.sections.ShapeDetailsSection;
import software.amazon.smithy.docgen.core.sections.ShapeSection;
import software.amazon.smithy.docgen.core.sections.ShapeSubheadingSection;
import software.amazon.smithy.docgen.core.writers.DocWriter;
import software.amazon.smithy.model.knowledge.ServiceIndex;
import software.amazon.smithy.model.knowledge.ServiceIndex.AuthSchemeMode;
import software.amazon.smithy.model.knowledge.TopDownIndex;
import software.amazon.smithy.model.shapes.ServiceShape;
import software.amazon.smithy.model.traits.synthetic.NoAuthTrait;
import software.amazon.smithy.utils.SmithyInternalApi;

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

writeAuthSection(context, writer, service);

writer.closeHeading();
writer.popState();
});
}

private void writeAuthSection(DocGenerationContext context, DocWriter writer, ServiceShape service) {
var authSchemes = DocgenUtils.getPrioritizedServiceAuth(context.model(), service);
if (authSchemes.isEmpty()) {
return;
}

writer.pushState(new AuthSection(context, service));
writer.openHeading("Auth");

var index = ServiceIndex.of(context.model());
writer.putContext("optional", index.getEffectiveAuthSchemes(service, AuthSchemeMode.NO_AUTH_AWARE)
.containsKey(NoAuthTrait.ID));
writer.putContext("multipleSchemes", authSchemes.size() > 1);
writer.write("""
Operations on the service ${?optional}may optionally${/optional}${^optional}MUST${/optional} \
be called with ${?multipleSchemes}one of the following priority-ordered auth schemes${/multipleSchemes}\
${^multipleSchemes}the following auth scheme${/multipleSchemes}. Additionally, authentication for \
individual operations may be optional${?multipleSchemes}, have a different priority order, support \
fewer schemes,${/multipleSchemes} or be disabled entirely.
""");

writer.openDefinitionList();

for (var scheme : authSchemes) {
var authTraitShape = context.model().expectShape(scheme);
var authTraitSymbol = context.symbolProvider().toSymbol(authTraitShape);

writer.pushState(new ShapeSection(context, authTraitShape));
writer.openDefinitionListItem(w -> w.write("$R", authTraitSymbol));

writer.injectSection(new ShapeSubheadingSection(context, authTraitShape));
writer.writeShapeDocs(authTraitShape, context.model());
writer.injectSection(new ShapeDetailsSection(context, authTraitShape));

writer.closeDefinitionListItem();
writer.popState();
}

writer.closeDefinitionList();
writer.closeHeading();
writer.popState();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import software.amazon.smithy.docgen.core.DocGenerationContext;
import software.amazon.smithy.docgen.core.DocIntegration;
import software.amazon.smithy.docgen.core.DocSettings;
import software.amazon.smithy.docgen.core.interceptors.ApiKeyAuthInterceptor;
import software.amazon.smithy.docgen.core.interceptors.DefaultValueInterceptor;
import software.amazon.smithy.docgen.core.interceptors.DeprecatedInterceptor;
import software.amazon.smithy.docgen.core.interceptors.ErrorFaultInterceptor;
Expand All @@ -20,6 +21,7 @@
import software.amazon.smithy.docgen.core.interceptors.NoReplaceBindingInterceptor;
import software.amazon.smithy.docgen.core.interceptors.NoReplaceOperationInterceptor;
import software.amazon.smithy.docgen.core.interceptors.NullabilityInterceptor;
import software.amazon.smithy.docgen.core.interceptors.OperationAuthInterceptor;
import software.amazon.smithy.docgen.core.interceptors.PaginationInterceptor;
import software.amazon.smithy.docgen.core.interceptors.PatternInterceptor;
import software.amazon.smithy.docgen.core.interceptors.RangeInterceptor;
Expand Down Expand Up @@ -71,6 +73,8 @@ public List<? extends CodeInterceptor<? extends CodeSection, DocWriter>> interce
// the ones at the end will be at the top of the rendered pages. Therefore, interceptors
// that provide more critical information should appear at the bottom of this list.
return List.of(
new OperationAuthInterceptor(),
new ApiKeyAuthInterceptor(),
new PaginationInterceptor(),
new RequestCompressionInterceptor(),
new NoReplaceBindingInterceptor(),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/

package software.amazon.smithy.docgen.core.interceptors;

import software.amazon.smithy.docgen.core.sections.ShapeDetailsSection;
import software.amazon.smithy.docgen.core.writers.DocWriter;
import software.amazon.smithy.model.traits.HttpApiKeyAuthTrait;
import software.amazon.smithy.model.traits.HttpApiKeyAuthTrait.Location;
import software.amazon.smithy.utils.CodeInterceptor;
import software.amazon.smithy.utils.Pair;
import software.amazon.smithy.utils.SmithyInternalApi;

/**
* Adds additional context to the description of api key auth based on the customized values.
*/
@SmithyInternalApi
public final class ApiKeyAuthInterceptor implements CodeInterceptor<ShapeDetailsSection, DocWriter> {
private static final Pair<String, String> AUTH_HEADER_REF = Pair.of(
"Authorization header", "https://datatracker.ietf.org/doc/html/rfc9110.html#section-11.4"
);

@Override
public Class<ShapeDetailsSection> sectionType() {
return ShapeDetailsSection.class;
}

@Override
public boolean isIntercepted(ShapeDetailsSection section) {
return section.shape().getId().equals(HttpApiKeyAuthTrait.ID);
}

@Override
public void write(DocWriter writer, String previousText, ShapeDetailsSection section) {
var service = section.context().model().expectShape(section.context().settings().service());
var trait = service.expectTrait(HttpApiKeyAuthTrait.class);
writer.putContext("name", trait.getName());
writer.putContext("location", trait.getIn().equals(Location.HEADER) ? "header" : "query string");
writer.putContext("scheme", trait.getScheme());
writer.putContext("authHeader", AUTH_HEADER_REF);
writer.write("""
The API key must be bound to the ${location:L} using the key ${name:`}.${?scheme} \
Additionally, the scheme used in the ${authHeader:R} must be ${scheme:`}.${/scheme}
$L""", previousText);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/

package software.amazon.smithy.docgen.core.interceptors;

import java.util.List;
import software.amazon.smithy.docgen.core.DocgenUtils;
import software.amazon.smithy.docgen.core.sections.ShapeDetailsSection;
import software.amazon.smithy.docgen.core.writers.DocWriter;
import software.amazon.smithy.docgen.core.writers.DocWriter.AdmonitionType;
import software.amazon.smithy.model.knowledge.ServiceIndex;
import software.amazon.smithy.model.knowledge.ServiceIndex.AuthSchemeMode;
import software.amazon.smithy.model.shapes.ToShapeId;
import software.amazon.smithy.model.traits.synthetic.NoAuthTrait;
import software.amazon.smithy.utils.CodeInterceptor;
import software.amazon.smithy.utils.SmithyInternalApi;

/**
* Adds a priority list of supported auth schemes for operations with optional auth or
* operations which don't support all of a service's auth schemes.
*/
@SmithyInternalApi
public final class OperationAuthInterceptor implements CodeInterceptor<ShapeDetailsSection, DocWriter> {
@Override
public Class<ShapeDetailsSection> sectionType() {
return ShapeDetailsSection.class;
}

@Override
public boolean isIntercepted(ShapeDetailsSection section) {
if (!section.shape().isOperationShape()) {
return false;
}
var index = ServiceIndex.of(section.context().model());
var service = section.context().settings().service();

// Only add the admonition if the service has auth in the first place.
var serviceAuth = index.getAuthSchemes(service);
if (serviceAuth.isEmpty()) {
return false;
}

// Only add the admonition if the operations' effective auth schemes differs
// from the total list of available auth schemes on the service.
var operationAuth = index.getEffectiveAuthSchemes(service, section.shape(), AuthSchemeMode.NO_AUTH_AWARE);
return !operationAuth.keySet().equals(serviceAuth.keySet());
}

@Override
public void write(DocWriter writer, String previousText, ShapeDetailsSection section) {
writer.writeWithNoFormatting(previousText);
writer.openAdmonition(AdmonitionType.IMPORTANT);

var index = ServiceIndex.of(section.context().model());
var service = section.context().settings().service();
var operation = section.shape();


var serviceAuth = DocgenUtils.getPrioritizedServiceAuth(section.context().model(), service);
var operationAuth = List.copyOf(
index.getEffectiveAuthSchemes(service, operation, AuthSchemeMode.MODELED).keySet());

if (serviceAuth.equals(operationAuth)) {
// If the total service auth and effective *modeled* operation auth are the same,
// that means that the operation just has optional auth since isIntercepted would
// return false otherwise. It would have been overly confusing to include this
// case in the big text block below.
writer.write("""
This operation may be optionally called without authentication.
""");
writer.closeAdmonition();
return;
}

var operationSchemes = operationAuth.stream()
.map(id -> section.context().symbolProvider().toSymbol(section.context().model().expectShape(id)))
.toList();

writer.putContext("optional", supportsNoAuth(index, service, section.shape()));
writer.putContext("schemes", operationSchemes);
writer.putContext("multipleSchemes", operationSchemes.size() > 1);

writer.write("""
${?schemes}This operation ${?optional}may optionally${/optional}${^optional}MUST${/optional} \
be called with ${?multipleSchemes}one of the following priority-ordered auth schemes${/multipleSchemes}\
${^multipleSchemes}the following auth scheme${/multipleSchemes}: \
${#schemes}${value:R}${^key.last}, ${/key.last}${/schemes}.${/schemes}\
${^schemes}${?optional}This operation must be called without authentication.${/optional}${/schemes}
""");
writer.closeAdmonition();
}

private boolean supportsNoAuth(ServiceIndex index, ToShapeId service, ToShapeId operation) {
return index.getEffectiveAuthSchemes(service, operation, AuthSchemeMode.NO_AUTH_AWARE)
.containsKey(NoAuthTrait.ID);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/

package software.amazon.smithy.docgen.core.sections;

import software.amazon.smithy.docgen.core.DocGenerationContext;
import software.amazon.smithy.model.shapes.ServiceShape;
import software.amazon.smithy.utils.CodeSection;
import software.amazon.smithy.utils.SmithyInternalApi;

/**
* Contains the documentation for the auth schemes that the service supports.
*
* @param context The context used to generate documentation.
* @param service The service whose documentation is being generated.
*
* @see ShapeSection to override documentation for individual auth schemes. The shape
* of the trait is passed as the shape.
*/
@SmithyInternalApi
public record AuthSection(DocGenerationContext context, ServiceShape service) implements CodeSection {
}
Loading

0 comments on commit 06ef518

Please sign in to comment.