Skip to content

Commit

Permalink
feat(experimentalIdentityAndAuth): update HttpAuthOption and `HttpA…
Browse files Browse the repository at this point in the history
…uthScheme` codegen

After upgrading to Smithy 1.37.0
(#906), upgrade to
`getEffectiveAuthSchemes` using `NO_AUTH_AWARE` and remove redundant
code.

For `HttpAuthOption`, allow any auth scheme to generate the
corresponding `HttpAuthOption` function regardless of registering it in
codegen.

For `HttpAuthScheme`, reduce code duplication by looking at "inherited"
`LanguageTarget` platforms (e.g. `REACT_NATIVE` inherits from
`BROWSER`). `httpAuthSchemes` is only written in a `runtimeConfig` if
the `IdentityProvider` and `Signer` are different between platforms, or
if the registered `HttpAuthScheme`s are different. `httpAuthSchemes` is
always written for `SHARED` as a default.

For the `httpAuthSchemes` property on the client config interface,
update it to `HttpAuthScheme[]` rather than
`IdentityProviderConfiguration`.
  • Loading branch information
syall committed Sep 5, 2023
1 parent 697310d commit d3071d0
Show file tree
Hide file tree
Showing 8 changed files with 250 additions and 149 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,19 @@
package software.amazon.smithy.typescript.codegen;

import java.nio.file.Paths;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.TreeMap;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import software.amazon.smithy.build.SmithyBuildException;
import software.amazon.smithy.codegen.core.CodegenException;
import software.amazon.smithy.codegen.core.SymbolProvider;
import software.amazon.smithy.model.Model;
import software.amazon.smithy.model.knowledge.ServiceIndex;
import software.amazon.smithy.model.shapes.ServiceShape;
import software.amazon.smithy.model.shapes.ShapeId;
import software.amazon.smithy.model.traits.Trait;
import software.amazon.smithy.typescript.codegen.auth.AuthUtils;
import software.amazon.smithy.typescript.codegen.auth.http.HttpAuthScheme;
import software.amazon.smithy.typescript.codegen.auth.http.HttpAuthSchemeProviderGenerator;
Expand Down Expand Up @@ -208,59 +207,60 @@ private void generateHttpAuthSchemeConfig(
TypeScriptWriter writer,
LanguageTarget target
) {
// feat(experimentalIdentityAndAuth): write the default imported HttpAuthSchemeProvider
if (target.equals(LanguageTarget.SHARED)) {
configs.put("httpAuthSchemeProvider", w -> {
String providerName = "default" + service.toShapeId().getName() + "HttpAuthSchemeProvider";
w.addRelativeImport(providerName, null, Paths.get(".", CodegenUtils.SOURCE_FOLDER,
HttpAuthSchemeProviderGenerator.HTTP_AUTH_FOLDER,
HttpAuthSchemeProviderGenerator.HTTP_AUTH_SCHEME_RESOLVER_MODULE));
w.write(providerName);
});
}

// feat(experimentalIdentityAndAuth): gather HttpAuthSchemes to generate
SupportedHttpAuthSchemesIndex index = new SupportedHttpAuthSchemesIndex(integrations);
SupportedHttpAuthSchemesIndex authIndex = new SupportedHttpAuthSchemesIndex(integrations);
ServiceIndex serviceIndex = ServiceIndex.of(model);
Map<ShapeId, Trait> authSchemeTraits = serviceIndex.getAuthSchemes(service);
Map<ShapeId, HttpAuthScheme> authSchemes = authSchemeTraits.entrySet().stream()
.filter(entry -> index.getHttpAuthScheme(entry.getKey()) != null)
.collect(Collectors.toMap(
entry -> entry.getKey(),
entry -> index.getHttpAuthScheme(entry.getKey())));
// feat(experimentalIdentityAndAuth): can be removed after changing to NO_AUTH_AWARE schemes
// The default set of HttpAuthSchemes is @smithy.api#noAuth on the shared runtime config
authSchemes.put(AuthUtils.NO_AUTH_ID, index.getHttpAuthScheme(AuthUtils.NO_AUTH_ID));
boolean shouldGenerateHttpAuthSchemes = index.getSupportedHttpAuthSchemes().values().stream().anyMatch(value ->
// If either an default identity or signer is configured for the target, generate auth schemes
value.getDefaultIdentityProviders().containsKey(target) || value.getDefaultSigners().containsKey(target));
if (!shouldGenerateHttpAuthSchemes) {
Map<ShapeId, HttpAuthScheme> allEffectiveHttpAuthSchemes =
AuthUtils.getAllEffectiveNoAuthAwareAuthSchemes(service, serviceIndex, authIndex);
List<HttpAuthSchemeTarget> targetAuthSchemes = getHttpAuthSchemeTargets(target, allEffectiveHttpAuthSchemes);

// Generate only if the "inherited" target is different than the current target
List<HttpAuthSchemeTarget> inheritedAuthSchemes = Collections.emptyList();
// Always generated the SHARED target
if (target.equals(LanguageTarget.SHARED)) {
// no-op
// NODE and BROWSER inherit from SHARED
} else if (target.equals(LanguageTarget.NODE) || target.equals(LanguageTarget.BROWSER)) {
inheritedAuthSchemes = getHttpAuthSchemeTargets(LanguageTarget.SHARED, allEffectiveHttpAuthSchemes);
// REACT_NATIVE inherits from BROWSER
} else if (target.equals(LanguageTarget.REACT_NATIVE)) {
inheritedAuthSchemes = getHttpAuthSchemeTargets(LanguageTarget.BROWSER, allEffectiveHttpAuthSchemes);
} else {
throw new CodegenException("Unhandled Language Target: " + target);
}

// If target and inherited auth schemes are equal, then don't generate target auth schemes.
if (targetAuthSchemes.equals(inheritedAuthSchemes)) {
return;
}
// feat(experimentalIdentityAndAuth): write the default imported HttpAuthSchemeProvider
configs.put("httpAuthSchemeProvider", w -> {
String providerName = "default" + service.toShapeId().getName() + "HttpAuthSchemeProvider";
w.addRelativeImport(providerName, null, Paths.get(".", CodegenUtils.SOURCE_FOLDER,
HttpAuthSchemeProviderGenerator.HTTP_AUTH_FOLDER,
HttpAuthSchemeProviderGenerator.HTTP_AUTH_SCHEME_RESOLVER_MODULE));
w.write(providerName);
});

// feat(experimentalIdentityAndAuth): write the default IdentityProviderConfiguration with configured
// HttpAuthSchemes
configs.put("httpAuthSchemes", w -> {
w.addDependency(TypeScriptDependency.EXPERIMENTAL_IDENTITY_AND_AUTH);
w.addImport("DefaultIdentityProviderConfiguration", null,
TypeScriptDependency.EXPERIMENTAL_IDENTITY_AND_AUTH);
w.openBlock("new DefaultIdentityProviderConfiguration([", "])", () -> {
Iterator<Entry<ShapeId, HttpAuthScheme>> iter = authSchemes.entrySet().iterator();
w.openBlock("[", "]", () -> {
Iterator<HttpAuthSchemeTarget> iter = targetAuthSchemes.iterator();
while (iter.hasNext()) {
Entry<ShapeId, HttpAuthScheme> entry = iter.next();
// Default IdentityProvider or HttpSigner, otherwise write {@code never} to force a TypeScript
// compilation failure.
Consumer<TypeScriptWriter> defaultIdentityProvider = entry.getValue()
.getDefaultIdentityProviders()
.getOrDefault(target, AuthUtils.DEFAULT_NEVER_WRITER);
Consumer<TypeScriptWriter> defaultSigner = entry.getValue()
.getDefaultSigners()
.getOrDefault(target, AuthUtils.DEFAULT_NEVER_WRITER);
HttpAuthSchemeTarget entry = iter.next();
w.write("""
{
schemeId: $S,
identityProvider: $C,
signer: $C,
}""",
entry.getKey(),
defaultIdentityProvider,
defaultSigner);
entry.httpAuthScheme.getSchemeId(),
entry.identityProvider,
entry.signer);
if (iter.hasNext()) {
w.writeInline(", ");
}
Expand All @@ -269,6 +269,105 @@ private void generateHttpAuthSchemeConfig(
});
}

private static class HttpAuthSchemeTarget {
public HttpAuthScheme httpAuthScheme;
public Consumer<TypeScriptWriter> identityProvider;
public Consumer<TypeScriptWriter> signer;

HttpAuthSchemeTarget(
HttpAuthScheme httpAuthScheme,
Consumer<TypeScriptWriter> identityProvider,
Consumer<TypeScriptWriter> signer
) {
this.httpAuthScheme = httpAuthScheme;
this.identityProvider = identityProvider;
this.signer = signer;
}

@Override
public boolean equals(Object other) {
if (!(other instanceof HttpAuthSchemeTarget)) {
return false;
}
HttpAuthSchemeTarget o = (HttpAuthSchemeTarget) other;
return httpAuthScheme.equals(o.httpAuthScheme)
&& identityProvider.equals(o.identityProvider)
&& signer.equals(o.signer);
}

@Override
public int hashCode() {
return super.hashCode();
}
}

private List<HttpAuthSchemeTarget> getHttpAuthSchemeTargets(
LanguageTarget target,
Map<ShapeId, HttpAuthScheme> httpAuthSchemes
) {
return getPartialHttpAuthSchemeTargets(target, httpAuthSchemes)
.values()
.stream()
.filter(httpAuthSchemeTarget ->
httpAuthSchemeTarget.identityProvider != null && httpAuthSchemeTarget.signer != null)
.toList();
}

private Map<ShapeId, HttpAuthSchemeTarget> getPartialHttpAuthSchemeTargets(
LanguageTarget target,
Map<ShapeId, HttpAuthScheme> httpAuthSchemes
) {
LanguageTarget inherited;
if (target.equals(LanguageTarget.SHARED)) {
// SHARED doesn't inherit any target, so inherited is null
inherited = null;
} else if (target.equals(LanguageTarget.NODE) || target.equals(LanguageTarget.BROWSER)) {
inherited = LanguageTarget.SHARED;
} else if (target.equals(LanguageTarget.REACT_NATIVE)) {
inherited = LanguageTarget.BROWSER;
} else {
throw new CodegenException("Unsupported Language Target: " + target);
}

Map<ShapeId, HttpAuthSchemeTarget> httpAuthSchemeTargets = inherited == null
// SHARED inherits no HttpAuthSchemeTargets
? new TreeMap<>()
// Otherwise, get inherited HttpAuthSchemeTargets
: getPartialHttpAuthSchemeTargets(inherited, httpAuthSchemes);

for (HttpAuthScheme httpAuthScheme : httpAuthSchemes.values()) {
// If HttpAuthScheme is not registered, skip code generation
if (httpAuthScheme == null) {
continue;
}

// Get identity provider and signer for the current target
Consumer<TypeScriptWriter> identityProvider =
httpAuthScheme.getDefaultIdentityProviders().get(target);
Consumer<TypeScriptWriter> signer =
httpAuthScheme.getDefaultSigners().get(target);

HttpAuthSchemeTarget existingEntry =
httpAuthSchemeTargets.get(httpAuthScheme.getSchemeId());

// If HttpAuthScheme is not added yet, add the entry
if (existingEntry == null) {
httpAuthSchemeTargets.put(httpAuthScheme.getSchemeId(),
new HttpAuthSchemeTarget(httpAuthScheme, identityProvider, signer));
continue;
}

// Mutate existing entry for identity provider and signer if available
if (identityProvider != null) {
existingEntry.identityProvider = identityProvider;
}
if (signer != null) {
existingEntry.signer = signer;
}
}
return httpAuthSchemeTargets;
}

private Map<String, Consumer<TypeScriptWriter>> getDefaultRuntimeConfigs(LanguageTarget target) {
switch (target) {
case NODE:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -305,13 +305,12 @@ private void generateClientDefaults() {
// feat(experimentalIdentityAndAuth): write httpAuthSchemes and httpAuthSchemeProvider into ClientDefaults
if (settings.getExperimentalIdentityAndAuth()) {
writer.addDependency(TypeScriptDependency.EXPERIMENTAL_IDENTITY_AND_AUTH);
writer.addImport("IdentityProviderConfiguration", null,
TypeScriptDependency.EXPERIMENTAL_IDENTITY_AND_AUTH);
writer.addImport("HttpAuthScheme", null, TypeScriptDependency.EXPERIMENTAL_IDENTITY_AND_AUTH);
writer.writeDocs("""
experimentalIdentityAndAuth: Configuration of HttpAuthSchemes for a client which provides \
default identity providers and signers per auth scheme.
@internal""");
writer.write("httpAuthSchemes?: IdentityProviderConfiguration;\n");
writer.write("httpAuthSchemes?: HttpAuthScheme[];\n");

String httpAuthSchemeProviderName = service.toShapeId().getName() + "HttpAuthSchemeProvider";
writer.addRelativeImport(httpAuthSchemeProviderName, null, Paths.get(".",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,41 @@

package software.amazon.smithy.typescript.codegen.auth;

import java.util.function.Consumer;
import java.util.Map;
import java.util.TreeMap;
import software.amazon.smithy.model.knowledge.ServiceIndex;
import software.amazon.smithy.model.knowledge.ServiceIndex.AuthSchemeMode;
import software.amazon.smithy.model.shapes.ServiceShape;
import software.amazon.smithy.model.shapes.ShapeId;
import software.amazon.smithy.typescript.codegen.TypeScriptWriter;
import software.amazon.smithy.typescript.codegen.auth.http.HttpAuthScheme;
import software.amazon.smithy.typescript.codegen.auth.http.SupportedHttpAuthSchemesIndex;
import software.amazon.smithy.utils.SmithyInternalApi;

/**
* Auth utility methods needed across Java packages.
*/
@SmithyInternalApi
public final class AuthUtils {
/**
* Writes out `never`, which will make TypeScript compilation fail if used as a value.
*/
public static final Consumer<TypeScriptWriter> DEFAULT_NEVER_WRITER = w -> w.write("never");

/**
* Should be replaced when the synthetic NoAuthTrait is released in Smithy.
*/
public static final ShapeId NO_AUTH_ID = ShapeId.from("smithy.api#noAuth");

private AuthUtils() {}

public static Map<ShapeId, HttpAuthScheme> getAllEffectiveNoAuthAwareAuthSchemes(
ServiceShape serviceShape,
ServiceIndex serviceIndex,
SupportedHttpAuthSchemesIndex authIndex
) {
Map<ShapeId, HttpAuthScheme> effectiveAuthSchemes = new TreeMap<>();
var serviceEffectiveAuthSchemes =
serviceIndex.getEffectiveAuthSchemes(serviceShape, AuthSchemeMode.NO_AUTH_AWARE);
for (ShapeId shapeId : serviceEffectiveAuthSchemes.keySet()) {
effectiveAuthSchemes.put(shapeId, authIndex.getHttpAuthScheme(shapeId));
}
for (var operation : serviceShape.getAllOperations()) {
var operationEffectiveAuthSchemes =
serviceIndex.getEffectiveAuthSchemes(serviceShape, operation, AuthSchemeMode.NO_AUTH_AWARE);
for (ShapeId shapeId : operationEffectiveAuthSchemes.keySet()) {
effectiveAuthSchemes.put(shapeId, authIndex.getHttpAuthScheme(shapeId));
}
}
return effectiveAuthSchemes;
}
}
Loading

0 comments on commit d3071d0

Please sign in to comment.