Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,159 @@
*/
package software.amazon.smithy.python.aws.codegen;

import java.util.Collections;
import java.util.List;
import software.amazon.smithy.aws.traits.auth.SigV4Trait;
import software.amazon.smithy.codegen.core.Symbol;
import software.amazon.smithy.model.shapes.ShapeId;
import software.amazon.smithy.model.traits.HttpApiKeyAuthTrait;
import software.amazon.smithy.python.codegen.ApplicationProtocol;
import software.amazon.smithy.python.codegen.CodegenUtils;
import software.amazon.smithy.python.codegen.ConfigProperty;
import software.amazon.smithy.python.codegen.DerivedProperty;
import software.amazon.smithy.python.codegen.GenerationContext;
import software.amazon.smithy.python.codegen.SmithyPythonDependency;
import software.amazon.smithy.python.codegen.integrations.AuthScheme;
import software.amazon.smithy.python.codegen.integrations.PythonIntegration;
import software.amazon.smithy.python.codegen.integrations.RuntimeClientPlugin;
import software.amazon.smithy.utils.SmithyInternalApi;

/**
* Adds support for AWS auth traits.
*/
@SmithyInternalApi
public class AwsAuthIntegration implements PythonIntegration {}
public class AwsAuthIntegration implements PythonIntegration {
private static final String SIGV4_OPTION_GENERATOR_NAME = "_generate_sigv4_option";

@Override
public List<RuntimeClientPlugin> getClientPlugins(GenerationContext context) {
var regionConfig = ConfigProperty.builder()
.name("region")
.type(Symbol.builder().name("str").build())
.documentation(" The AWS region to connect to. The configured region is used to "
+ "determine the service endpoint.")
.build();

return List.of(
RuntimeClientPlugin.builder()
.servicePredicate((model, service) -> service.hasTrait(SigV4Trait.class))
.addConfigProperty(ConfigProperty.builder()
// TODO: Naming of this config RE: backwards compatability/migation considerations
.name("aws_credentials_identity_resolver")
.documentation("Resolves AWS Credentials. Required for operations that use Sigv4 Auth.")
.type(Symbol.builder()
.name("IdentityResolver[AWSCredentialsIdentity, IdentityProperties]")
.addReference(Symbol.builder()
.addDependency(SmithyPythonDependency.SMITHY_CORE)
.name("IdentityResolver")
.namespace("smithy_core.aio.interfaces.identity", ".")
.build())
.addReference(Symbol.builder()
.addDependency(AwsPythonDependency.SMITHY_AWS_CORE)
.name("AWSCredentialsIdentity")
.namespace("smithy_aws_core.identity", ".")
.build())
.addReference(Symbol.builder()
.addDependency(SmithyPythonDependency.SMITHY_CORE)
.name("IdentityProperties")
.namespace("smithy_core.interfaces.identity", ".")
.build())
.build())
// TODO: Initialize with the provider chain?
.nullable(true)
.build())
.addConfigProperty(regionConfig)
.authScheme(new Sigv4AuthScheme())
.build()
);
}

@Override
public void customize(GenerationContext context) {
if (!hasSigV4Auth(context)) {
return;
}
var trait = context.settings().service(context.model()).expectTrait(SigV4Trait.class);
var params = CodegenUtils.getHttpAuthParamsSymbol(context.settings());
var resolver = CodegenUtils.getHttpAuthSchemeResolverSymbol(context.settings());

// Add a function that generates the http auth option for api key auth.
// This needs to be generated because there's modeled parameters that
// must be accounted for.
Comment on lines +84 to +85
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tragic

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the alternative would be to add signingService as an auth param. We would then need a way to wire that up during the request. It could be an undocumented config property or have it somewhere else.

context.writerDelegator().useFileWriter(resolver.getDefinitionFile(), resolver.getNamespace(), writer -> {
writer.addDependency(SmithyPythonDependency.SMITHY_HTTP);
writer.addImport("smithy_http.aio.interfaces.auth", "HTTPAuthOption");
writer.pushState();

writer.write("""
def $1L(auth_params: $2T) -> HTTPAuthOption | None:
return HTTPAuthOption(
scheme_id=$3S,
identity_properties={},
signer_properties={
"service": $4S,
"region": auth_params.region
}
)
""",
SIGV4_OPTION_GENERATOR_NAME,
params,
SigV4Trait.ID.toString(),
trait.getName());
writer.popState();
});
}

private boolean hasSigV4Auth(GenerationContext context) {
var service = context.settings().service(context.model());
return service.hasTrait(SigV4Trait.class);
}

/**
* The AuthScheme representing api key auth.
*/
private static final class Sigv4AuthScheme implements AuthScheme {

@Override
public ShapeId getAuthTrait() {
return SigV4Trait.ID;
}

@Override
public ApplicationProtocol getApplicationProtocol() {
return ApplicationProtocol.createDefaultHttpApplicationProtocol();
}

@Override
public List<DerivedProperty> getAuthProperties() {
return List.of(
DerivedProperty.builder()
.name("region")
.source(DerivedProperty.Source.CONFIG)
.type(Symbol.builder().name("str").build())
.sourcePropertyName("region")
.build()
);
}


@Override
public Symbol getAuthOptionGenerator(GenerationContext context) {
var resolver = CodegenUtils.getHttpAuthSchemeResolverSymbol(context.settings());
return Symbol.builder()
.name(SIGV4_OPTION_GENERATOR_NAME)
.namespace(resolver.getNamespace(), ".")
.definitionFile(resolver.getDefinitionFile())
.build();
}

@Override
public Symbol getAuthSchemeSymbol(GenerationContext context) {
return Symbol.builder()
.name("SigV4AuthScheme")
.namespace("smithy_aws_core.auth", ".")
.addDependency(AwsPythonDependency.SMITHY_AWS_CORE)
.build();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,7 @@ async def _handle_attempt(
for option in auth_options:
if option.scheme_id in config.http_auth_schemes:
auth_option = option
break

signer: HTTPSigner[Any, Any] | None = None
identity: Identity | None = None
Expand Down
2 changes: 2 additions & 0 deletions packages/smithy-aws-core/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"smithy-core",
"smithy-http",
"aws-sdk-signers"
]

[build-system]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
# SPDX-License-Identifier: Apache-2.0

from .sigv4 import SigV4AuthScheme

__all__ = ("SigV4AuthScheme",)
54 changes: 54 additions & 0 deletions packages/smithy-aws-core/src/smithy_aws_core/auth/sigv4.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
# SPDX-License-Identifier: Apache-2.0
from dataclasses import dataclass
from typing import Protocol

from smithy_aws_core.identity import AWSCredentialsIdentity
from smithy_core.aio.interfaces.identity import IdentityResolver
from smithy_core.exceptions import SmithyIdentityException
from smithy_core.interfaces.identity import IdentityProperties
from smithy_http.aio.interfaces.auth import HTTPAuthScheme, HTTPSigner
from aws_sdk_signers import SigV4SigningProperties, SigV4Signer


class SigV4Config(Protocol):
aws_credentials_identity_resolver: (
IdentityResolver[AWSCredentialsIdentity, IdentityProperties] | None
)


@dataclass(init=False)
class SigV4AuthScheme(
HTTPAuthScheme[
AWSCredentialsIdentity, SigV4Config, IdentityProperties, SigV4SigningProperties
]
):
"""SigV4 AuthScheme."""

scheme_id: str
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
scheme_id: str
scheme_id: Final = "aws.auth#sigv4"

Well this should also be a ShapeID but that's probably a bit more involved to do just now.

Copy link
Contributor Author

@alextwoods alextwoods Mar 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I think we'd need to update the HttpAuthScheme protocol as well for that. I wasn't able to update this to Final without changing the HttpAuthScheme as well, which breaks with Final (since nothing is initialized on it).

For now, I've left the type as str but moved the initialization of it out of __init__

signer: HTTPSigner[AWSCredentialsIdentity, SigV4SigningProperties]

def __init__(
self,
*,
signer: HTTPSigner[AWSCredentialsIdentity, SigV4SigningProperties]
| None = None,
) -> None:
"""Constructor.
:param identity_resolver: The identity resolver to extract the api key identity.
:param signer: The signer used to sign the request.
"""
self.scheme_id = "aws.auth#sigv4"
# TODO: There are type mismatches in the signature of the "sign" method.
self.signer = signer or SigV4Signer() # type: ignore

def identity_resolver(
self, *, config: SigV4Config
) -> IdentityResolver[AWSCredentialsIdentity, IdentityProperties]:
if not config.aws_credentials_identity_resolver:
raise SmithyIdentityException(
"Attempted to use SigV4 auth, but aws_credentials_identity_resolver was not "
"set on the config."
)
return config.aws_credentials_identity_resolver
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
# SPDX-License-Identifier: Apache-2.0
from .environment_credentials_resolver import EnvironmentCredentialsResolver
from .static_credentials_resolver import StaticCredentialsResolver

__all__ = ("EnvironmentCredentialsResolver", "StaticCredentialsResolver")
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
# SPDX-License-Identifier: Apache-2.0
import os

from smithy_aws_core.identity import AWSCredentialsIdentity
from smithy_core.aio.interfaces.identity import IdentityResolver
from smithy_core.exceptions import SmithyIdentityException
from smithy_core.interfaces.identity import IdentityProperties


class EnvironmentCredentialsResolver(
IdentityResolver[AWSCredentialsIdentity, IdentityProperties]
):
"""Resolves AWS Credentials from system environment variables."""

async def get_identity(
self, *, identity_properties: IdentityProperties
) -> AWSCredentialsIdentity:
access_key_id = os.getenv("AWS_ACCESS_KEY_ID")
secret_access_key = os.getenv("AWS_SECRET_ACCESS_KEY")
session_token = os.getenv("AWS_SESSION_TOKEN")
account_id = os.getenv("AWS_ACCOUNT_ID")

if access_key_id is None or secret_access_key is None:
raise SmithyIdentityException(
"AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY are required"
)

return AWSCredentialsIdentity(
access_key_id=access_key_id,
secret_access_key=secret_access_key,
session_token=session_token,
account_id=account_id,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
# SPDX-License-Identifier: Apache-2.0
from smithy_aws_core.identity import AWSCredentialsIdentity
from smithy_core.aio.interfaces.identity import IdentityResolver
from smithy_core.interfaces.identity import IdentityProperties


class StaticCredentialsResolver(
IdentityResolver[AWSCredentialsIdentity, IdentityProperties]
):
"""Resolve Static AWS Credentials."""

def __init__(self, *, credentials: AWSCredentialsIdentity) -> None:
self._credentials = credentials

async def get_identity(
self, *, identity_properties: IdentityProperties
) -> AWSCredentialsIdentity:
return self._credentials
9 changes: 8 additions & 1 deletion packages/smithy-aws-core/src/smithy_aws_core/identity.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from smithy_core.identity import Identity


class AWSCredentialIdentity(Identity):
class AWSCredentialsIdentity(Identity):
"""Container for AWS authentication credentials."""

def __init__(
Expand All @@ -25,6 +25,7 @@ def __init__(
secret_access_key: str,
session_token: str | None = None,
expiration: datetime | None = None,
account_id: str | None = None,
) -> None:
"""Initialize the AWSCredentialIdentity.

Expand All @@ -35,11 +36,13 @@ def __init__(
the supplied credentials.
:param expiration: The expiration time of the identity. If time zone is
provided, it is updated to UTC. The value must always be in UTC.
:param account_id: The AWS account's ID.
"""
super().__init__(expiration=expiration)
self._access_key_id: str = access_key_id
self._secret_access_key: str = secret_access_key
self._session_token: str | None = session_token
self._account_id: str | None = account_id

@property
def access_key_id(self) -> str:
Expand All @@ -52,3 +55,7 @@ def secret_access_key(self) -> str:
@property
def session_token(self) -> str | None:
return self._session_token

@property
def account_id(self) -> str | None:
return self._account_id
2 changes: 1 addition & 1 deletion packages/smithy-http/src/smithy_http/aio/auth/apikey.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ def identity_resolver(
) -> IdentityResolver[ApiKeyIdentity, IdentityProperties]:
if not config.api_key_identity_resolver:
raise SmithyIdentityException(
"Attempted to use API key auth, but api_key_identity_resolver was not"
"Attempted to use API key auth, but api_key_identity_resolver was not "
"set on the config."
)
return config.api_key_identity_resolver
Expand Down
8 changes: 7 additions & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading