Remove AWS SDK V1 references (#52990)

* refactor: remove references to aws sdk v1

* refactor(aws): remove legacy error conversion for s3

* refactor(awsoidc): remove unused clientv1

* refactor: code review suggestions

* refactor: move xml helper functions into alpnproxy package

* chore(gomod): move into lambda direct dep

* chore: fix import order
This commit is contained in:
Gabriel Corado
2025-03-18 15:43:12 +00:00
committed by GitHub
parent ec58d4f585
commit bcf1625f9a
19 changed files with 438 additions and 881 deletions
+1
View File
@@ -65,6 +65,7 @@ require (
github.com/aws/aws-sdk-go-v2/service/iam v1.40.0
github.com/aws/aws-sdk-go-v2/service/identitystore v1.28.0
github.com/aws/aws-sdk-go-v2/service/kms v1.38.0
github.com/aws/aws-sdk-go-v2/service/lambda v1.70.1
github.com/aws/aws-sdk-go-v2/service/memorydb v1.26.0
github.com/aws/aws-sdk-go-v2/service/opensearch v1.46.0
github.com/aws/aws-sdk-go-v2/service/organizations v1.38.0
+2
View File
@@ -926,6 +926,8 @@ github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.15 h1:moLQUoVq91Liq
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.15/go.mod h1:ZH34PJUc8ApjBIfgQCFvkWcUDBtl/WTD+uiYHjd8igA=
github.com/aws/aws-sdk-go-v2/service/kms v1.38.0 h1:+2/0Cq0R/audJhwM1GpJMg8X1TTrMKDFRLO5RMaNRU0=
github.com/aws/aws-sdk-go-v2/service/kms v1.38.0/go.mod h1:cQn6tAF77Di6m4huxovNM7NVAozWTZLsDRp9t8Z/WYk=
github.com/aws/aws-sdk-go-v2/service/lambda v1.70.1 h1:EabaKQAptxXAeSL0sXKqfupPe/CpH965wqoloUK0aMM=
github.com/aws/aws-sdk-go-v2/service/lambda v1.70.1/go.mod h1:c27kk10S36lBYgbG1jR3opn4OAS5Y/4wjJa1GiHK/X4=
github.com/aws/aws-sdk-go-v2/service/memorydb v1.26.0 h1:nO9RCZnfAIF5q43IDLWtf7vu/l16RKzeTkv5GObkyME=
github.com/aws/aws-sdk-go-v2/service/memorydb v1.26.0/go.mod h1:pfuDC5zBwunXdE44WT1PRbtzuXWGohKFcFLtv+ezI6k=
github.com/aws/aws-sdk-go-v2/service/opensearch v1.46.0 h1:eR65kYpNlKpGkkvg+A83hc0hpk2CHappaz1JAUCcxVs=
+6 -6
View File
@@ -19,7 +19,7 @@
package events
import (
"github.com/aws/aws-sdk-go/aws/endpoints"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/gravitational/teleport/api/types"
)
@@ -31,14 +31,14 @@ const (
)
var (
fipsToAWS = map[types.ClusterAuditConfigSpecV2_FIPSEndpointState]endpoints.FIPSEndpointState{
types.ClusterAuditConfigSpecV2_FIPS_UNSET: endpoints.FIPSEndpointStateUnset,
types.ClusterAuditConfigSpecV2_FIPS_ENABLED: endpoints.FIPSEndpointStateEnabled,
types.ClusterAuditConfigSpecV2_FIPS_DISABLED: endpoints.FIPSEndpointStateDisabled,
fipsToAWS = map[types.ClusterAuditConfigSpecV2_FIPSEndpointState]aws.FIPSEndpointState{
types.ClusterAuditConfigSpecV2_FIPS_UNSET: aws.FIPSEndpointStateUnset,
types.ClusterAuditConfigSpecV2_FIPS_ENABLED: aws.FIPSEndpointStateEnabled,
types.ClusterAuditConfigSpecV2_FIPS_DISABLED: aws.FIPSEndpointStateDisabled,
}
)
// FIPSProtoStateToAWSState converts a FIPS proto state to an aws endpoints.FIPSEndpointState
func FIPSProtoStateToAWSState(state types.ClusterAuditConfigSpecV2_FIPSEndpointState) endpoints.FIPSEndpointState {
func FIPSProtoStateToAWSState(state types.ClusterAuditConfigSpecV2_FIPSEndpointState) aws.FIPSEndpointState {
return fipsToAWS[state]
}
-124
View File
@@ -1,124 +0,0 @@
/*
* Teleport
* Copyright (C) 2023 Gravitational, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package awsoidc
import (
"context"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/credentials/stscreds"
"github.com/aws/aws-sdk-go/aws/endpoints"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/gravitational/trace"
"github.com/gravitational/teleport/api/types"
utilsaws "github.com/gravitational/teleport/api/utils/aws"
"github.com/gravitational/teleport/lib/modules"
"github.com/gravitational/teleport/lib/utils/aws/stsutils"
)
// FetchToken returns the token.
func (j IdentityToken) FetchToken(ctx credentials.Context) ([]byte, error) {
return []byte(j), nil
}
// IntegrationTokenGenerator is an interface that indicates which APIs are required to generate an Integration Token.
type IntegrationTokenGenerator interface {
// GetIntegration returns the specified integration resources.
GetIntegration(ctx context.Context, name string) (types.Integration, error)
// GenerateAWSOIDCToken generates a token to be used to execute an AWS OIDC Integration action.
GenerateAWSOIDCToken(ctx context.Context, integration string) (string, error)
}
// NewSessionV1 creates a new AWS Session for the region using the integration as source of credentials.
// This session is usable for AWS SDK Go V1.
func NewSessionV1(ctx context.Context, client IntegrationTokenGenerator, region string, integrationName string) (*session.Session, error) {
if region != "" {
if err := utilsaws.IsValidRegion(region); err != nil {
return nil, trace.Wrap(err)
}
}
integration, err := client.GetIntegration(ctx, integrationName)
if err != nil {
return nil, trace.Wrap(err)
}
awsOIDCIntegration := integration.GetAWSOIDCIntegrationSpec()
if awsOIDCIntegration == nil {
return nil, trace.BadParameter("invalid integration subkind, expected awsoidc, got %s", integration.GetSubKind())
}
useFIPSEndpoint := endpoints.FIPSEndpointStateUnset
if modules.GetModules().IsBoringBinary() {
useFIPSEndpoint = endpoints.FIPSEndpointStateEnabled
}
sess, err := session.NewSessionWithOptions(session.Options{
SharedConfigState: session.SharedConfigDisable,
Config: aws.Config{
UseFIPSEndpoint: useFIPSEndpoint,
},
})
if err != nil {
return nil, trace.Wrap(err)
}
// AWS SDK calls FetchToken everytime the session is no longer valid (or there's no session).
// Generating a token here and using it as a Static would make this token valid for the Max Duration Session for the current AWS Role (usually, 1 hour).
// Instead, it generates a token everytime the Session's client requests a new token, ensuring it always receives a fresh one.
var integrationTokenFetcher IntegrationTokenFetcher = func(ctx context.Context) ([]byte, error) {
token, err := client.GenerateAWSOIDCToken(ctx, integrationName)
return []byte(token), trace.Wrap(err)
}
stsSTS := stsutils.NewV1(sess)
roleProvider := stscreds.NewWebIdentityRoleProviderWithOptions(
stsSTS,
awsOIDCIntegration.RoleARN,
"",
integrationTokenFetcher,
)
awsCredentials := credentials.NewCredentials(roleProvider)
session, err := session.NewSessionWithOptions(session.Options{
SharedConfigState: session.SharedConfigDisable,
Config: aws.Config{
Region: aws.String(region),
Credentials: awsCredentials,
UseFIPSEndpoint: useFIPSEndpoint,
},
})
if err != nil {
return nil, trace.Wrap(err)
}
return session, nil
}
// IntegrationTokenFetcher handles dynamic token generation using a callback function.
// Useful to embed as a [stscreds.TokenFetcher].
type IntegrationTokenFetcher func(context.Context) ([]byte, error)
// FetchToken returns a token by calling the callback function.
func (genFn IntegrationTokenFetcher) FetchToken(ctx context.Context) ([]byte, error) {
token, err := genFn(ctx)
return token, trace.Wrap(err)
}
-129
View File
@@ -1,129 +0,0 @@
/*
* Teleport
* Copyright (C) 2023 Gravitational, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package awsoidc
import (
"context"
"testing"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/google/uuid"
"github.com/gravitational/trace"
"github.com/stretchr/testify/require"
"github.com/gravitational/teleport/api/types"
)
type mockIntegrationsTokenGenerator struct {
proxies []types.Server
integrations map[string]types.Integration
tokenCallsCount int
}
// GetIntegration returns the specified integration resources.
func (m *mockIntegrationsTokenGenerator) GetIntegration(ctx context.Context, name string) (types.Integration, error) {
if ig, found := m.integrations[name]; found {
return ig, nil
}
return nil, trace.NotFound("integration not found")
}
// GetProxies returns a list of registered proxies.
func (m *mockIntegrationsTokenGenerator) GetProxies() ([]types.Server, error) {
return m.proxies, nil
}
// GenerateAWSOIDCToken generates a token to be used to execute an AWS OIDC Integration action.
func (m *mockIntegrationsTokenGenerator) GenerateAWSOIDCToken(ctx context.Context, integration string) (string, error) {
m.tokenCallsCount++
return uuid.NewString(), nil
}
func TestNewSessionV1(t *testing.T) {
ctx := context.Background()
dummyIntegration, err := types.NewIntegrationAWSOIDC(
types.Metadata{Name: "myawsintegration"},
&types.AWSOIDCIntegrationSpecV1{
RoleARN: "arn:aws:sts::123456789012:role/TestRole",
},
)
require.NoError(t, err)
dummyProxy, err := types.NewServer(
"proxy-123", types.KindProxy,
types.ServerSpecV2{
PublicAddrs: []string{"https://localhost:3080/"},
},
)
require.NoError(t, err)
for _, tt := range []struct {
name string
region string
integration string
tokenFetchCount int
expectedErr require.ErrorAssertionFunc
sessionValidator func(*testing.T, *session.Session)
}{
{
name: "valid",
region: "us-dummy-1",
integration: "myawsintegration",
expectedErr: require.NoError,
sessionValidator: func(t *testing.T, s *session.Session) {
require.Equal(t, aws.String("us-dummy-1"), s.Config.Region)
},
},
{
name: "valid with empty region",
region: "",
integration: "myawsintegration",
expectedErr: require.NoError,
sessionValidator: func(t *testing.T, s *session.Session) {
require.Equal(t, "", aws.StringValue(s.Config.Region))
},
},
{
name: "not found error when integration is missing",
region: "us-dummy-1",
integration: "not-found",
expectedErr: notFoundCheck,
},
} {
t.Run(tt.name, func(t *testing.T) {
mockTokenGenertor := &mockIntegrationsTokenGenerator{
proxies: []types.Server{dummyProxy},
integrations: map[string]types.Integration{
dummyIntegration.GetName(): dummyIntegration,
},
}
awsSessionOut, err := NewSessionV1(ctx, mockTokenGenertor, tt.region, tt.integration)
tt.expectedErr(t, err)
if tt.sessionValidator != nil {
tt.sessionValidator(t, awsSessionOut)
}
require.Zero(t, tt.tokenFetchCount)
})
}
}
@@ -25,9 +25,9 @@ import (
"testing"
"time"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/sts"
ststypes "github.com/aws/aws-sdk-go-v2/service/sts/types"
"github.com/aws/aws-sdk-go/aws"
"github.com/google/uuid"
"github.com/jonboulle/clockwork"
"github.com/stretchr/testify/assert"
+32 -2
View File
@@ -19,6 +19,8 @@
package alpnproxy
import (
"bytes"
"encoding/xml"
"log/slog"
"net/http"
"strings"
@@ -26,6 +28,7 @@ import (
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/credentials"
"github.com/aws/aws-sdk-go-v2/service/sts"
smithyxml "github.com/aws/smithy-go/encoding/xml"
"github.com/gravitational/trace"
"github.com/gravitational/teleport"
@@ -216,12 +219,12 @@ func (m *AWSAccessMiddleware) handleSTSResponse(response *http.Response) error {
}
func unmarshalAssumeRoleResponse(body []byte) (*sts.AssumeRoleOutput, error) {
if !awsutils.IsXMLOfLocalName(body, "AssumeRoleResponse") {
if !isXMLOfLocalName(body, "AssumeRoleResponse") {
return nil, trace.NotFound("not AssumeRoleResponse")
}
var assumedRole sts.AssumeRoleOutput
if err := awsutils.UnmarshalXMLChildNode(&assumedRole, body, "AssumeRoleResult"); err != nil {
if err := unmarshalXMLChildNode(&assumedRole, body, "AssumeRoleResult"); err != nil {
return nil, trace.Wrap(err)
}
if assumedRole.AssumedRoleUser == nil {
@@ -232,3 +235,30 @@ func unmarshalAssumeRoleResponse(body []byte) (*sts.AssumeRoleOutput, error) {
}
return &assumedRole, nil
}
// isXMLOfLocalName returns true if the root XML has the provided (local) name.
func isXMLOfLocalName(data []byte, wantLocalName string) bool {
st, err := smithyxml.FetchRootElement(xml.NewDecoder(bytes.NewReader(data)))
if err == nil && st.Name.Local == wantLocalName {
return true
}
return false
}
// unmarshalXMLChildNode decodes the XML-encoded data and stores the child node
// with the specified name to v, where v is a pointer to an AWS SDK v2 struct.
func unmarshalXMLChildNode(v interface{}, data []byte, childName string) error {
decoder := xml.NewDecoder(bytes.NewReader(data))
st, err := smithyxml.FetchRootElement(decoder)
if err != nil {
return trace.Wrap(err)
}
nodeDecoder := smithyxml.WrapNodeDecoder(decoder, st)
childElem, err := nodeDecoder.GetElement(childName)
if err != nil {
return trace.Wrap(err)
}
return trace.Wrap(decoder.DecodeElement(v, &childElem))
}
+121 -42
View File
@@ -19,7 +19,7 @@
package alpnproxy
import (
"context"
"bytes"
"encoding/xml"
"net/http"
"net/http/httptest"
@@ -30,6 +30,7 @@ import (
"github.com/aws/aws-sdk-go-v2/credentials"
"github.com/aws/aws-sdk-go-v2/service/sts"
ststypes "github.com/aws/aws-sdk-go-v2/service/sts/types"
"github.com/gravitational/trace"
"github.com/stretchr/testify/require"
awsutils "github.com/gravitational/teleport/lib/utils/aws"
@@ -39,8 +40,8 @@ func TestAWSAccessMiddleware(t *testing.T) {
t.Parallel()
assumedRoleARN := "arn:aws:sts::123456789012:assumed-role/role-name/role-session-name"
localProxyCred := credentials.NewStaticCredentialsProvider("local-proxy", "local-proxy-secret", "")
assumedRoleCred := credentials.NewStaticCredentialsProvider("assumed-role", "assumed-role-secret", "assumed-role-token")
localCred := aws.Credentials{AccessKeyID: "local-proxy", SecretAccessKey: "local-proxy-secret"}
assumedRoleCred := aws.Credentials{AccessKeyID: "assumed-role", SecretAccessKey: "assumed-role-secret", SessionToken: "assumed-role-token"}
m := &AWSAccessMiddleware{
AWSCredentialsProvider: credentials.NewStaticCredentialsProvider("local-proxy", "local-proxy-secret", ""),
@@ -48,11 +49,10 @@ func TestAWSAccessMiddleware(t *testing.T) {
require.NoError(t, m.CheckAndSetDefaults())
stsRequestByLocalProxyCred := httptest.NewRequest(http.MethodPost, "http://sts.us-east-2.amazonaws.com", nil)
awsutils.NewSignerV2(localProxyCred, "sts").Sign(stsRequestByLocalProxyCred, nil, "sts", "us-west-1", time.Now())
awsutils.NewSigner("sts").SignHTTP(t.Context(), localCred, stsRequestByLocalProxyCred, awsutils.EmptyPayloadHash, "sts", "us-west-1", time.Now())
requestByAssumedRole := httptest.NewRequest(http.MethodGet, "http://s3.amazonaws.com", nil)
awsutils.NewSignerV2(assumedRoleCred, "s3").Sign(requestByAssumedRole, nil, "s3", "us-west-1", time.Now())
awsutils.NewSigner("s3").SignHTTP(t.Context(), assumedRoleCred, requestByAssumedRole, awsutils.EmptyPayloadHash, "s3", "us-west-1", time.Now())
t.Run("request no authorization", func(t *testing.T) {
recorder := httptest.NewRecorder()
@@ -99,34 +99,101 @@ func TestAWSAccessMiddleware(t *testing.T) {
})
}
func assumeRoleResponse(t *testing.T, roleARN string, provider aws.CredentialsProvider) *http.Response {
func TestUnmarshalAssumeRoleResponse(t *testing.T) {
want := &sts.AssumeRoleOutput{
AssumedRoleUser: &ststypes.AssumedRoleUser{
Arn: aws.String("some-arn"),
},
Credentials: &ststypes.Credentials{
AccessKeyId: aws.String("some-access-key-id"),
SecretAccessKey: aws.String("some-secret-access-key"),
SessionToken: aws.String("some-session-token"),
Expiration: aws.Time(time.Unix(1234567890, 0).UTC()),
},
}
body := []byte(`<AssumeRoleResponse xmlns="https://sts.amazonaws.com/doc/2011-06-15/">
<AssumeRoleResult>
<Credentials>
<SecretAccessKey>some-secret-access-key</SecretAccessKey>
<SessionToken>some-session-token</SessionToken>
<AccessKeyId>some-access-key-id</AccessKeyId>
<Expiration>2009-02-13T23:31:30Z</Expiration>
</Credentials>
<AssumedRoleUser>
<Arn>some-arn</Arn>
</AssumedRoleUser>
</AssumeRoleResult>
<ResponseMetadata>
<StatusCode>200</StatusCode>
<RequestID>some-request-id</RequestID>
</ResponseMetadata>
</AssumeRoleResponse>`)
actual, err := unmarshalAssumeRoleResponse(body)
require.NoError(t, err)
require.Equal(t, want, actual)
t.Run("invalid xml", func(t *testing.T) {
_, err := unmarshalAssumeRoleResponse([]byte(""))
require.Error(t, err)
})
}
// IdentityResult represents the identitiy result of an AWS response.
type IdentityResult struct {
ARN string `xml:"Arn"`
}
// ResponseMetadata contains the metadata of a AWS response.
type ResponseMetadata struct {
RequestID string `xml:"RequestID"`
StatusCode int `xml:"StatusCode"`
}
// AssumeRoleResult contains the assume role result.
type AssumeRoleResult struct {
// AssumedRoleUser is the assumed user.
AssumedRoleUser IdentityResult `xml:"AssumedRoleUser"`
// Credentials is the generated credentials.
Credentials ststypes.Credentials `xml:"Credentials"`
}
// AssumeRoleResponse is the response of assume role.
type AssumeRoleResponse struct {
// AssumeRoleResult is the resulting response from assume role.
AssumeRoleResult AssumeRoleResult `xml:"AssumeRoleResult"`
// Response is the response metadata.
Response ResponseMetadata `xml:"ResponseMetadata"`
}
// GetCallerIdentityResponse is the response of get caller identity call.
type GetCallerIdentityResponse struct {
// AssumeRoleResult is the resulting response from assume role.
GetCallerIdentityResult IdentityResult `xml:"GetCallerIdentityResult"`
// Response is the response metadata.
Response ResponseMetadata `xml:"ResponseMetadata"`
}
func assumeRoleResponse(t *testing.T, roleARN string, creds aws.Credentials) *http.Response {
t.Helper()
credValue, err := provider.Retrieve(context.Background())
require.NoError(t, err)
body, err := awsutils.MarshalXML(
xml.Name{
Local: "AssumeRoleResponse",
Space: "https://sts.amazonaws.com/doc/2011-06-15/",
},
map[string]any{
"AssumeRoleResult": sts.AssumeRoleOutput{
AssumedRoleUser: &ststypes.AssumedRoleUser{
Arn: aws.String(roleARN),
},
Credentials: &ststypes.Credentials{
AccessKeyId: aws.String(credValue.AccessKeyID),
SecretAccessKey: aws.String(credValue.SecretAccessKey),
SessionToken: aws.String(credValue.SessionToken),
},
body, err := marshalXML("AssumeRoleResponse", "https://sts.amazonaws.com/doc/2011-06-15/", AssumeRoleResponse{
AssumeRoleResult: AssumeRoleResult{
AssumedRoleUser: IdentityResult{
ARN: roleARN,
},
"ResponseMetadata": map[string]any{
"StatusCode": http.StatusOK,
"RequestID": "22222222-3333-3333-3333-333333333333",
Credentials: ststypes.Credentials{
AccessKeyId: aws.String(creds.AccessKeyID),
SecretAccessKey: aws.String(creds.SecretAccessKey),
SessionToken: aws.String(creds.SessionToken),
},
},
)
Response: ResponseMetadata{
StatusCode: http.StatusOK,
RequestID: "22222222-3333-3333-3333-333333333333",
},
})
require.NoError(t, err)
return fakeHTTPResponse(http.StatusOK, body)
}
@@ -134,21 +201,15 @@ func assumeRoleResponse(t *testing.T, roleARN string, provider aws.CredentialsPr
func getCallerIdentityResponse(t *testing.T, roleARN string) *http.Response {
t.Helper()
body, err := awsutils.MarshalXML(
xml.Name{
Local: "GetCallerIdentityResponse",
Space: "https://sts.amazonaws.com/doc/2011-06-15/",
body, err := marshalXML("GetCallerIdentityResponse", "https://sts.amazonaws.com/doc/2011-06-15/", GetCallerIdentityResponse{
GetCallerIdentityResult: IdentityResult{
ARN: roleARN,
},
map[string]any{
"GetCallerIdentityResult": sts.GetCallerIdentityOutput{
Arn: aws.String(roleARN),
},
"ResponseMetadata": map[string]any{
"StatusCode": http.StatusOK,
"RequestID": "22222222-3333-3333-3333-333333333333",
},
Response: ResponseMetadata{
StatusCode: http.StatusOK,
RequestID: "22222222-3333-3333-3333-333333333333",
},
)
})
require.NoError(t, err)
return fakeHTTPResponse(http.StatusOK, body)
}
@@ -159,3 +220,21 @@ func fakeHTTPResponse(code int, body []byte) *http.Response {
recorder.WriteHeader(code)
return recorder.Result()
}
// marshalXML marshals the provided root name and a map of children in XML with
// default indent (prefix "", indent " ").
func marshalXML(root string, namespace string, v any) ([]byte, error) {
var buf bytes.Buffer
encoder := xml.NewEncoder(&buf)
encoder.Indent("", " ")
err := encoder.EncodeElement(v, xml.StartElement{
Name: xml.Name{Local: root},
Attr: []xml.Attr{
{Name: xml.Name{Local: "xmlns"}, Value: namespace},
},
})
if err != nil {
return nil, trace.Wrap(err)
}
return buf.Bytes(), nil
}
+6 -6
View File
@@ -19,20 +19,20 @@
package aws
import (
"bytes"
"net/http"
"testing"
"time"
"github.com/aws/aws-sdk-go/aws/credentials"
v4 "github.com/aws/aws-sdk-go/aws/signer/v4"
"github.com/aws/aws-sdk-go-v2/aws"
v4 "github.com/aws/aws-sdk-go-v2/aws/signer/v4"
"github.com/stretchr/testify/require"
awsutils "github.com/gravitational/teleport/lib/utils/aws"
)
func TestResolveEndpoints(t *testing.T) {
signer := v4.NewSigner(credentials.NewStaticCredentials("fakeClientKeyID", "fakeClientSecret", ""))
creds := aws.Credentials{AccessKeyID: "fakeClientKeyID", SecretAccessKey: "fakeClientSecret"}
signer := v4.NewSigner()
region := "us-east-1"
now := time.Now()
@@ -40,7 +40,7 @@ func TestResolveEndpoints(t *testing.T) {
req, err := http.NewRequest("GET", "http://localhost", nil)
require.NoError(t, err)
_, err = signer.Sign(req, bytes.NewReader(nil), "ecr", "us-east-1", now)
err = signer.SignHTTP(t.Context(), creds, req, awsutils.EmptyPayloadHash, "ecr", "us-east-1", now)
require.NoError(t, err)
_, err = resolveEndpoint(req, awsutils.AuthorizationHeader)
@@ -52,7 +52,7 @@ func TestResolveEndpoints(t *testing.T) {
require.NoError(t, err)
req.Header.Set("X-Forwarded-Host", "some-service.us-east-1.amazonaws.com")
_, err = signer.Sign(req, bytes.NewReader(nil), "some-service", region, now)
err = signer.SignHTTP(t.Context(), creds, req, awsutils.EmptyPayloadHash, "some-service", region, now)
require.NoError(t, err)
endpoint, err := resolveEndpoint(req, awsutils.AuthorizationHeader)
+6 -1
View File
@@ -165,6 +165,11 @@ func (s *signerHandler) serveCommonRequest(sessCtx *common.SessionContext, w htt
return trace.Wrap(err)
}
reqCloneForAudit, err := cloneRequest(unsignedReq)
if err != nil {
return trace.Wrap(err)
}
awsCfg, err := s.AWSConfigProvider.GetConfig(s.closeContext, re.SigningRegion,
awsconfig.WithDetailedAssumeRole(awsconfig.AssumeRole{
RoleARN: sessCtx.Identity.RouteToApp.AWSRoleARN,
@@ -189,7 +194,7 @@ func (s *signerHandler) serveCommonRequest(sessCtx *common.SessionContext, w htt
}
recorder := httplib.NewResponseStatusRecorder(w)
s.fwd.ServeHTTP(recorder, signedReq)
s.emitAudit(sessCtx, unsignedReq, uint32(recorder.Status()), re)
s.emitAudit(sessCtx, reqCloneForAudit, uint32(recorder.Status()), re)
return nil
}
+197 -219
View File
@@ -31,16 +31,13 @@ import (
"testing"
"time"
credentialsv2 "github.com/aws/aws-sdk-go-v2/credentials"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/aws/client"
"github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/dynamodb"
"github.com/aws/aws-sdk-go/service/lambda"
"github.com/aws/aws-sdk-go/service/s3"
"github.com/aws/aws-sdk-go/service/sts"
"github.com/aws/aws-sdk-go-v2/aws"
transporthttp "github.com/aws/aws-sdk-go-v2/aws/transport/http"
"github.com/aws/aws-sdk-go-v2/credentials"
"github.com/aws/aws-sdk-go-v2/service/dynamodb"
"github.com/aws/aws-sdk-go-v2/service/lambda"
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/aws/aws-sdk-go-v2/service/sts"
"github.com/google/go-cmp/cmp"
"github.com/gravitational/trace"
"github.com/jonboulle/clockwork"
@@ -66,47 +63,51 @@ func TestMain(m *testing.M) {
os.Exit(m.Run())
}
type makeRequest func(url string, provider client.ConfigProvider, awsHost string) error
type makeRequest func(ctx context.Context, url string, region string, provider aws.CredentialsProvider, awsHost string) error
func s3Request(url string, provider client.ConfigProvider, awsHost string) error {
return s3RequestWithTransport(url, provider, &requestByHTTPSProxy{xForwardedHost: awsHost})
func s3Request(ctx context.Context, url string, region string, provider aws.CredentialsProvider, awsHost string) error {
return s3RequestWithTransport(ctx, url, region, provider, &requestByHTTPSProxy{xForwardedHost: awsHost})
}
func s3RequestByAssumedRole(url string, provider client.ConfigProvider, awsHost string) error {
return s3RequestWithTransport(url, provider, &requestByAssumedRoleTransport{xForwardedHost: awsHost})
func s3RequestByAssumedRole(ctx context.Context, url string, region string, provider aws.CredentialsProvider, awsHost string) error {
return s3RequestWithTransport(ctx, url, region, provider, &requestByAssumedRoleTransport{xForwardedHost: awsHost})
}
func s3RequestWithTransport(url string, provider client.ConfigProvider, transport http.RoundTripper) error {
s3Client := s3.New(provider, &aws.Config{
Endpoint: &url,
MaxRetries: aws.Int(0),
func s3RequestWithTransport(ctx context.Context, url string, region string, provider aws.CredentialsProvider, transport http.RoundTripper) error {
s3Client := s3.New(s3.Options{
Credentials: provider,
BaseEndpoint: &url,
Region: region,
RetryMaxAttempts: 0,
HTTPClient: &http.Client{
Transport: transport,
Timeout: 5 * time.Second,
},
})
_, err := s3Client.ListBuckets(&s3.ListBucketsInput{})
_, err := s3Client.ListBuckets(ctx, &s3.ListBucketsInput{})
return err
}
func dynamoRequest(url string, provider client.ConfigProvider, awsHost string) error {
return dynamoRequestWithTransport(url, provider, &requestByHTTPSProxy{xForwardedHost: awsHost})
func dynamoRequest(ctx context.Context, url string, region string, provider aws.CredentialsProvider, awsHost string) error {
return dynamoRequestWithTransport(ctx, url, region, provider, &requestByHTTPSProxy{xForwardedHost: awsHost})
}
func dynamoRequestByAssumedRole(url string, provider client.ConfigProvider, awsHost string) error {
return dynamoRequestWithTransport(url, provider, &requestByAssumedRoleTransport{xForwardedHost: awsHost})
func dynamoRequestByAssumedRole(ctx context.Context, url string, region string, provider aws.CredentialsProvider, awsHost string) error {
return dynamoRequestWithTransport(ctx, url, region, provider, &requestByAssumedRoleTransport{xForwardedHost: awsHost})
}
func dynamoRequestWithTransport(url string, provider client.ConfigProvider, transport http.RoundTripper) error {
dynamoClient := dynamodb.New(provider, &aws.Config{
Endpoint: &url,
MaxRetries: aws.Int(0),
func dynamoRequestWithTransport(ctx context.Context, url string, region string, provider aws.CredentialsProvider, transport http.RoundTripper) error {
dynamoClient := dynamodb.New(dynamodb.Options{
Credentials: provider,
BaseEndpoint: &url,
Region: region,
RetryMaxAttempts: 0,
HTTPClient: &http.Client{
Transport: transport,
Timeout: 5 * time.Second,
},
})
_, err := dynamoClient.Scan(&dynamodb.ScanInput{
_, err := dynamoClient.Scan(ctx, &dynamodb.ScanInput{
TableName: aws.String("test-table"),
})
return err
@@ -116,30 +117,32 @@ func dynamoRequestWithTransport(url string, provider client.ConfigProvider, tran
// size. Use a 1MB limit instead of the actual 70MB limit.
const maxTestHTTPRequestBodySize = 1 << 20
func maxSizeExceededRequest(url string, provider client.ConfigProvider, awsHost string) error {
func maxSizeExceededRequest(ctx context.Context, url string, region string, provider aws.CredentialsProvider, awsHost string) error {
// fake an upload that's too large
payload := strings.Repeat("x", maxTestHTTPRequestBodySize)
return lambdaRequestWithPayload(url, provider, payload, &requestByHTTPSProxy{xForwardedHost: awsHost})
return lambdaRequestWithPayload(ctx, url, region, provider, payload, &requestByHTTPSProxy{xForwardedHost: awsHost})
}
func lambdaRequest(url string, provider client.ConfigProvider, awsHost string) error {
func lambdaRequest(ctx context.Context, url string, region string, provider aws.CredentialsProvider, awsHost string) error {
// fake a zip file with 70% of the max limit. Lambda will base64 encode it,
// which bloats it up, and our proxy should still handle it.
const size = (maxTestHTTPRequestBodySize * 7) / 10
payload := strings.Repeat("x", size)
return lambdaRequestWithPayload(url, provider, payload, &requestByHTTPSProxy{xForwardedHost: awsHost})
return lambdaRequestWithPayload(ctx, url, region, provider, payload, &requestByHTTPSProxy{xForwardedHost: awsHost})
}
func lambdaRequestWithPayload(url string, provider client.ConfigProvider, payload string, transport http.RoundTripper) error {
lambdaClient := lambda.New(provider, &aws.Config{
Endpoint: &url,
MaxRetries: aws.Int(0),
func lambdaRequestWithPayload(ctx context.Context, url string, region string, provider aws.CredentialsProvider, payload string, transport http.RoundTripper) error {
lambdaClient := lambda.New(lambda.Options{
Credentials: provider,
BaseEndpoint: &url,
Region: region,
RetryMaxAttempts: 0,
HTTPClient: &http.Client{
Timeout: 5 * time.Second,
Transport: transport,
},
})
_, err := lambdaClient.UpdateFunctionCode(&lambda.UpdateFunctionCodeInput{
_, err := lambdaClient.UpdateFunctionCode(ctx, &lambda.UpdateFunctionCodeInput{
FunctionName: aws.String("fakeFunc"),
ZipFile: []byte(payload),
})
@@ -147,17 +150,19 @@ func lambdaRequestWithPayload(url string, provider client.ConfigProvider, payloa
}
func assumeRoleRequest(requestDuration time.Duration) makeRequest {
return func(url string, provider client.ConfigProvider, awsHost string) error {
stsClient := stsutils.NewV1(provider, &aws.Config{
Endpoint: &url,
MaxRetries: aws.Int(0),
return func(ctx context.Context, url string, region string, provider aws.CredentialsProvider, awsHost string) error {
stsClient := stsutils.NewFromConfig(aws.Config{
Credentials: provider,
BaseEndpoint: &url,
Region: region,
RetryMaxAttempts: 0,
HTTPClient: &http.Client{
Timeout: 5 * time.Second,
Transport: &requestByHTTPSProxy{xForwardedHost: awsHost},
},
})
_, err := stsClient.AssumeRole(&sts.AssumeRoleInput{
DurationSeconds: aws.Int64(int64(requestDuration.Seconds())),
_, err := stsClient.AssumeRole(ctx, &sts.AssumeRoleInput{
DurationSeconds: aws.Int32(int32(requestDuration.Seconds())),
RoleSessionName: aws.String("test-session"),
RoleArn: aws.String("arn:aws:iam::123456789012:role/test-role"),
})
@@ -191,9 +196,9 @@ func (r requestByAssumedRoleTransport) RoundTrip(req *http.Request) (*http.Respo
func hasStatusCode(wantStatusCode int) require.ErrorAssertionFunc {
return func(t require.TestingT, err error, msgAndArgs ...interface{}) {
var apiErr awserr.RequestFailure
require.ErrorAs(t, err, &apiErr, msgAndArgs...)
require.Equal(t, wantStatusCode, apiErr.StatusCode(), msgAndArgs...)
var respErr *transporthttp.ResponseError
require.ErrorAs(t, err, &respErr, msgAndArgs...)
require.Equal(t, wantStatusCode, respErr.Response.StatusCode, msgAndArgs...)
}
}
@@ -224,47 +229,44 @@ func TestAWSSignerHandler(t *testing.T) {
require.NoError(t, err)
tests := []struct {
name string
app types.Application
awsClientSession *session.Session
awsConfigProvider awsconfig.Provider
request makeRequest
advanceClock time.Duration
wantHost string
wantAuthCredService string
wantAuthCredRegion string
wantAuthCredKeyID string
wantEventType events.AuditEvent
wantAssumedRole string
skipVerifySignature bool
verifySentRequest func(*testing.T, *http.Request)
errAssertionFns []require.ErrorAssertionFunc
name string
app types.Application
awsCredentialsProvider aws.CredentialsProvider
awsRegion string
awsConfigProvider awsconfig.Provider
request makeRequest
advanceClock time.Duration
wantHost string
wantAuthCredService string
wantAuthCredRegion string
wantAuthCredKeyID string
wantEventType events.AuditEvent
wantAssumedRole string
skipVerifySignature bool
verifySentRequest func(*testing.T, *http.Request)
errAssertionFns []require.ErrorAssertionFunc
}{
{
name: "s3 access",
app: consoleApp,
awsClientSession: session.Must(session.NewSession(&aws.Config{
Credentials: staticAWSCredentialsForClient,
Region: aws.String("us-west-2"),
})),
request: s3Request,
wantHost: "s3.us-west-2.amazonaws.com",
wantAuthCredKeyID: "FAKEACCESSKEYID",
wantAuthCredService: "s3",
wantAuthCredRegion: "us-west-2",
wantEventType: &events.AppSessionRequest{},
name: "s3 access",
app: consoleApp,
awsCredentialsProvider: staticAWSCredentialsForClient,
awsRegion: "us-west-2",
request: s3Request,
wantHost: "s3.us-west-2.amazonaws.com",
wantAuthCredKeyID: "FAKEACCESSKEYID",
wantAuthCredService: "s3",
wantAuthCredRegion: "us-west-2",
wantEventType: &events.AppSessionRequest{},
errAssertionFns: []require.ErrorAssertionFunc{
require.NoError,
},
},
{
name: "s3 access with integration",
app: consoleAppWithIntegration,
awsClientSession: session.Must(session.NewSession(&aws.Config{
Credentials: staticAWSCredentialsForClient,
Region: aws.String("us-west-2"),
})),
request: s3Request,
name: "s3 access with integration",
app: consoleAppWithIntegration,
awsCredentialsProvider: staticAWSCredentialsForClient,
awsRegion: "us-west-2",
request: s3Request,
awsConfigProvider: &mocks.AWSConfigProvider{
OIDCIntegrationClient: &mocks.FakeOIDCIntegrationClient{
Integration: awsOIDCIntegration,
@@ -281,144 +283,126 @@ func TestAWSSignerHandler(t *testing.T) {
},
},
{
name: "s3 access with different region",
app: consoleApp,
awsClientSession: session.Must(session.NewSession(&aws.Config{
Credentials: staticAWSCredentialsForClient,
Region: aws.String("us-west-1"),
})),
request: s3Request,
wantHost: "s3.us-west-1.amazonaws.com",
wantAuthCredKeyID: "FAKEACCESSKEYID",
wantAuthCredService: "s3",
wantAuthCredRegion: "us-west-1",
wantEventType: &events.AppSessionRequest{},
name: "s3 access with different region",
app: consoleApp,
awsCredentialsProvider: staticAWSCredentialsForClient,
awsRegion: "us-west-1",
request: s3Request,
wantHost: "s3.us-west-1.amazonaws.com",
wantAuthCredKeyID: "FAKEACCESSKEYID",
wantAuthCredService: "s3",
wantAuthCredRegion: "us-west-1",
wantEventType: &events.AppSessionRequest{},
errAssertionFns: []require.ErrorAssertionFunc{
require.NoError,
},
},
{
name: "s3 access missing credentials",
app: consoleApp,
awsClientSession: session.Must(session.NewSession(&aws.Config{
Credentials: credentials.AnonymousCredentials,
Region: aws.String("us-west-1"),
})),
request: s3Request,
name: "s3 access missing credentials",
app: consoleApp,
awsCredentialsProvider: aws.AnonymousCredentials{},
awsRegion: "us-west-1",
request: s3Request,
errAssertionFns: []require.ErrorAssertionFunc{
hasStatusCode(http.StatusBadRequest),
},
},
{
name: "s3 access by assumed role",
app: consoleApp,
awsClientSession: session.Must(session.NewSession(&aws.Config{
Credentials: staticAWSCredentialsForAssumedRole,
Region: aws.String("us-west-2"),
})),
request: s3RequestByAssumedRole,
wantHost: "s3.us-west-2.amazonaws.com",
wantAuthCredKeyID: assumedRoleKeyID, // not using service's access key ID
wantAuthCredService: "s3",
wantAuthCredRegion: "us-west-2",
wantEventType: &events.AppSessionRequest{},
wantAssumedRole: fakeAssumedRoleARN, // verifies assumed role is recorded in audit
skipVerifySignature: true, // not re-signing
name: "s3 access by assumed role",
app: consoleApp,
awsCredentialsProvider: staticAWSCredentialsForAssumedRole,
awsRegion: "us-west-2",
request: s3RequestByAssumedRole,
wantHost: "s3.us-west-2.amazonaws.com",
wantAuthCredKeyID: assumedRoleKeyID, // not using service's access key ID
wantAuthCredService: "s3",
wantAuthCredRegion: "us-west-2",
wantEventType: &events.AppSessionRequest{},
wantAssumedRole: fakeAssumedRoleARN, // verifies assumed role is recorded in audit
skipVerifySignature: true, // not re-signing
errAssertionFns: []require.ErrorAssertionFunc{
require.NoError,
},
},
{
name: "DynamoDB access",
app: consoleApp,
awsClientSession: session.Must(session.NewSession(&aws.Config{
Credentials: staticAWSCredentialsForClient,
Region: aws.String("us-east-1"),
})),
request: dynamoRequest,
wantHost: "dynamodb.us-east-1.amazonaws.com",
wantAuthCredKeyID: "FAKEACCESSKEYID",
wantAuthCredService: "dynamodb",
wantAuthCredRegion: "us-east-1",
wantEventType: &events.AppSessionDynamoDBRequest{},
name: "DynamoDB access",
app: consoleApp,
awsCredentialsProvider: staticAWSCredentialsForClient,
awsRegion: "us-east-1",
request: dynamoRequest,
wantHost: "dynamodb.us-east-1.amazonaws.com",
wantAuthCredKeyID: "FAKEACCESSKEYID",
wantAuthCredService: "dynamodb",
wantAuthCredRegion: "us-east-1",
wantEventType: &events.AppSessionDynamoDBRequest{},
errAssertionFns: []require.ErrorAssertionFunc{
require.NoError,
},
},
{
name: "DynamoDB access with different region",
app: consoleApp,
awsClientSession: session.Must(session.NewSession(&aws.Config{
Credentials: staticAWSCredentialsForClient,
Region: aws.String("us-west-1"),
})),
request: dynamoRequest,
wantHost: "dynamodb.us-west-1.amazonaws.com",
wantAuthCredKeyID: "FAKEACCESSKEYID",
wantAuthCredService: "dynamodb",
wantAuthCredRegion: "us-west-1",
wantEventType: &events.AppSessionDynamoDBRequest{},
name: "DynamoDB access with different region",
app: consoleApp,
awsCredentialsProvider: staticAWSCredentialsForClient,
awsRegion: "us-west-1",
request: dynamoRequest,
wantHost: "dynamodb.us-west-1.amazonaws.com",
wantAuthCredKeyID: "FAKEACCESSKEYID",
wantAuthCredService: "dynamodb",
wantAuthCredRegion: "us-west-1",
wantEventType: &events.AppSessionDynamoDBRequest{},
errAssertionFns: []require.ErrorAssertionFunc{
require.NoError,
},
},
{
name: "DynamoDB access missing credentials",
app: consoleApp,
awsClientSession: session.Must(session.NewSession(&aws.Config{
Credentials: credentials.AnonymousCredentials,
Region: aws.String("us-west-1"),
})),
request: dynamoRequest,
name: "DynamoDB access missing credentials",
app: consoleApp,
awsCredentialsProvider: aws.AnonymousCredentials{},
awsRegion: "us-west-1",
request: dynamoRequest,
errAssertionFns: []require.ErrorAssertionFunc{
hasStatusCode(http.StatusBadRequest),
},
},
{
name: "DynamoDB access by assumed role",
app: consoleApp,
awsClientSession: session.Must(session.NewSession(&aws.Config{
Credentials: staticAWSCredentialsForAssumedRole,
Region: aws.String("us-east-1"),
})),
request: dynamoRequestByAssumedRole,
wantHost: "dynamodb.us-east-1.amazonaws.com",
wantAuthCredKeyID: assumedRoleKeyID, // not using service's access key ID
wantAuthCredService: "dynamodb",
wantAuthCredRegion: "us-east-1",
wantEventType: &events.AppSessionDynamoDBRequest{},
wantAssumedRole: fakeAssumedRoleARN, // verifies assumed role is recorded in audit
skipVerifySignature: true, // not re-signing
name: "DynamoDB access by assumed role",
app: consoleApp,
awsCredentialsProvider: staticAWSCredentialsForAssumedRole,
awsRegion: "us-east-1",
request: dynamoRequestByAssumedRole,
wantHost: "dynamodb.us-east-1.amazonaws.com",
wantAuthCredKeyID: assumedRoleKeyID, // not using service's access key ID
wantAuthCredService: "dynamodb",
wantAuthCredRegion: "us-east-1",
wantEventType: &events.AppSessionDynamoDBRequest{},
wantAssumedRole: fakeAssumedRoleARN, // verifies assumed role is recorded in audit
skipVerifySignature: true, // not re-signing
errAssertionFns: []require.ErrorAssertionFunc{
require.NoError,
},
},
{
name: "Lambda access",
app: consoleApp,
awsClientSession: session.Must(session.NewSession(&aws.Config{
Credentials: staticAWSCredentialsForClient,
Region: aws.String("us-east-1"),
})),
request: lambdaRequest,
wantHost: "lambda.us-east-1.amazonaws.com",
wantAuthCredKeyID: "FAKEACCESSKEYID",
wantAuthCredService: "lambda",
wantAuthCredRegion: "us-east-1",
wantEventType: &events.AppSessionRequest{},
name: "Lambda access",
app: consoleApp,
awsCredentialsProvider: staticAWSCredentialsForClient,
awsRegion: "us-east-1",
request: lambdaRequest,
wantHost: "lambda.us-east-1.amazonaws.com",
wantAuthCredKeyID: "FAKEACCESSKEYID",
wantAuthCredService: "lambda",
wantAuthCredRegion: "us-east-1",
wantEventType: &events.AppSessionRequest{},
errAssertionFns: []require.ErrorAssertionFunc{
require.NoError,
},
},
{
name: "Request exceeding max size",
app: consoleApp,
awsClientSession: session.Must(session.NewSession(&aws.Config{
Credentials: staticAWSCredentialsForClient,
Region: aws.String("us-east-1"),
})),
request: maxSizeExceededRequest,
wantHost: "lambda.us-east-1.amazonaws.com",
name: "Request exceeding max size",
app: consoleApp,
awsCredentialsProvider: staticAWSCredentialsForClient,
awsRegion: "us-east-1",
request: maxSizeExceededRequest,
wantHost: "lambda.us-east-1.amazonaws.com",
errAssertionFns: []require.ErrorAssertionFunc{
// TODO(gavin): change this to [http.StatusRequestEntityTooLarge]
// after updating [trace.ErrorToCode].
@@ -426,52 +410,46 @@ func TestAWSSignerHandler(t *testing.T) {
},
},
{
name: "AssumeRole success (shorter identity duration)",
app: consoleApp,
awsClientSession: session.Must(session.NewSession(&aws.Config{
Credentials: staticAWSCredentialsForClient,
Region: aws.String("us-east-1"),
})),
request: assumeRoleRequest(2 * time.Hour),
advanceClock: 10 * time.Minute,
wantHost: "sts.amazonaws.com",
wantAuthCredKeyID: "FAKEACCESSKEYID",
wantAuthCredService: "sts",
wantAuthCredRegion: "us-east-1",
wantEventType: &events.AppSessionRequest{},
verifySentRequest: verifyAssumeRoleDuration(50 * time.Minute), // 1h (suite default for identity) - 10m
name: "AssumeRole success (shorter identity duration)",
app: consoleApp,
awsCredentialsProvider: staticAWSCredentialsForClient,
awsRegion: "us-east-1",
request: assumeRoleRequest(2 * time.Hour),
advanceClock: 10 * time.Minute,
wantHost: "sts.amazonaws.com",
wantAuthCredKeyID: "FAKEACCESSKEYID",
wantAuthCredService: "sts",
wantAuthCredRegion: "us-east-1",
wantEventType: &events.AppSessionRequest{},
verifySentRequest: verifyAssumeRoleDuration(50 * time.Minute), // 1h (suite default for identity) - 10m
errAssertionFns: []require.ErrorAssertionFunc{
require.NoError,
},
},
{
name: "AssumeRole success (shorter requested duration)",
app: consoleApp,
awsClientSession: session.Must(session.NewSession(&aws.Config{
Credentials: staticAWSCredentialsForClient,
Region: aws.String("us-east-1"),
})),
request: assumeRoleRequest(32 * time.Minute),
wantHost: "sts.amazonaws.com",
wantAuthCredKeyID: "FAKEACCESSKEYID",
wantAuthCredService: "sts",
wantAuthCredRegion: "us-east-1",
wantEventType: &events.AppSessionRequest{},
verifySentRequest: verifyAssumeRoleDuration(32 * time.Minute), // matches the request
name: "AssumeRole success (shorter requested duration)",
app: consoleApp,
awsCredentialsProvider: staticAWSCredentialsForClient,
awsRegion: "us-east-1",
request: assumeRoleRequest(32 * time.Minute),
wantHost: "sts.amazonaws.com",
wantAuthCredKeyID: "FAKEACCESSKEYID",
wantAuthCredService: "sts",
wantAuthCredRegion: "us-east-1",
wantEventType: &events.AppSessionRequest{},
verifySentRequest: verifyAssumeRoleDuration(32 * time.Minute), // matches the request
errAssertionFns: []require.ErrorAssertionFunc{
require.NoError,
},
},
{
name: "AssumeRole denied",
app: consoleApp,
awsClientSession: session.Must(session.NewSession(&aws.Config{
Credentials: staticAWSCredentialsForClient,
Region: aws.String("us-east-1"),
})),
request: assumeRoleRequest(2 * time.Hour),
wantHost: "sts.amazonaws.com",
advanceClock: 50 * time.Minute, // identity is expiring in 10m which is less than minimum
name: "AssumeRole denied",
app: consoleApp,
awsCredentialsProvider: staticAWSCredentialsForClient,
awsRegion: "us-east-1",
request: assumeRoleRequest(2 * time.Hour),
wantHost: "sts.amazonaws.com",
advanceClock: 50 * time.Minute, // identity is expiring in 10m which is less than minimum
errAssertionFns: []require.ErrorAssertionFunc{
// the request is 403 forbidden by Teleport, so the mock AWS handler won't be sent anything.
hasStatusCode(http.StatusForbidden),
@@ -498,7 +476,7 @@ func TestAWSSignerHandler(t *testing.T) {
// check that the signature is valid.
if !tc.skipVerifySignature {
err := awsutils.VerifyAWSSignature(r,
credentialsv2.NewStaticCredentialsProvider(tc.wantAuthCredKeyID, "secret", "token"),
credentials.NewStaticCredentialsProvider(tc.wantAuthCredKeyID, "secret", "token"),
)
if !assert.NoError(t, err) {
http.Error(w, err.Error(), trace.ErrorToCode(err))
@@ -521,7 +499,7 @@ func TestAWSSignerHandler(t *testing.T) {
suite := createSuite(t, mockAwsHandler, tc.app, fakeClock, awsCfgProvider)
fakeClock.Advance(tc.advanceClock)
err := tc.request(suite.URL, tc.awsClientSession, tc.wantHost)
err := tc.request(t.Context(), suite.URL, tc.awsRegion, tc.awsCredentialsProvider, tc.wantHost)
for _, assertFn := range tc.errAssertionFns {
assertFn(t, err)
}
@@ -625,8 +603,8 @@ func mustNewRequest(t *testing.T, method, url string, body io.Reader) *http.Requ
const assumedRoleKeyID = "assumedRoleKeyID"
var (
staticAWSCredentialsForAssumedRole = credentials.NewStaticCredentials(assumedRoleKeyID, "assumedRoleKeySecret", "")
staticAWSCredentialsForClient = credentials.NewStaticCredentials("fakeClientKeyID", "fakeClientSecret", "")
staticAWSCredentialsForAssumedRole = credentials.NewStaticCredentialsProvider(assumedRoleKeyID, "assumedRoleKeySecret", "")
staticAWSCredentialsForClient = credentials.NewStaticCredentialsProvider("fakeClientKeyID", "fakeClientSecret", "")
)
type suite struct {
+1 -6
View File
@@ -69,11 +69,6 @@ const (
// azureVirtualMachineCacheTTL is the default TTL for Azure virtual machine
// cache entries.
azureVirtualMachineCacheTTL = 5 * time.Minute
// emptyPayloadHash is the SHA-256 for an empty element (as in echo -n | sha256sum).
// PresignHTTP requires the hash of the body, but when there is no body we hash the empty string.
// https://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-header-based-auth.html
emptyPayloadHash = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
)
// Auth defines interface for creating auth tokens and TLS configurations.
@@ -1323,7 +1318,7 @@ func (r *awsRedisIAMTokenRequest) toSignedRequestURI(ctx context.Context) (strin
if err != nil {
return "", trace.Wrap(err)
}
signedURI, _, err := signer.PresignHTTP(ctx, creds, req, emptyPayloadHash, r.serviceName, r.region, r.clock.Now())
signedURI, _, err := signer.PresignHTTP(ctx, creds, req, awsutils.EmptyPayloadHash, r.serviceName, r.region, r.clock.Now())
if err != nil {
return "", trace.Wrap(err)
}
+26 -27
View File
@@ -22,10 +22,9 @@ import (
"context"
"testing"
awsv2 "github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/ec2"
ec2types "github.com/aws/aws-sdk-go-v2/service/ec2/types"
"github.com/aws/aws-sdk-go/aws"
"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/require"
@@ -58,16 +57,16 @@ func (m *mockEC2Client) DescribeInstances(ctx context.Context, input *ec2.Descri
func instanceMatches(inst ec2types.Instance, filters []ec2types.Filter) bool {
allMatched := true
for _, filter := range filters {
name := awsv2.ToString(filter.Name)
name := aws.ToString(filter.Name)
val := filter.Values[0]
if name == AWSInstanceStateName && inst.State.Name != ec2types.InstanceStateNameRunning {
return false
}
for _, tag := range inst.Tags {
if awsv2.ToString(tag.Key) != name[4:] {
if aws.ToString(tag.Key) != name[4:] {
continue
}
allMatched = allMatched && awsv2.ToString(tag.Value) != val
allMatched = allMatched && aws.ToString(tag.Value) != val
}
}
@@ -91,7 +90,7 @@ func TestNewEC2InstanceFetcherTags(t *testing.T) {
},
expectedFilters: []ec2types.Filter{
{
Name: awsv2.String(AWSInstanceStateName),
Name: aws.String(AWSInstanceStateName),
Values: []string{string(ec2types.InstanceStateNameRunning)},
},
},
@@ -105,11 +104,11 @@ func TestNewEC2InstanceFetcherTags(t *testing.T) {
},
expectedFilters: []ec2types.Filter{
{
Name: awsv2.String(AWSInstanceStateName),
Name: aws.String(AWSInstanceStateName),
Values: []string{string(ec2types.InstanceStateNameRunning)},
},
{
Name: awsv2.String("tag:hello"),
Name: aws.String("tag:hello"),
Values: []string{"other"},
},
},
@@ -156,15 +155,15 @@ func TestEC2Watcher(t *testing.T) {
ctx := context.Background()
present := ec2types.Instance{
InstanceId: awsv2.String("instance-present"),
InstanceId: aws.String("instance-present"),
Tags: []ec2types.Tag{
{
Key: awsv2.String("teleport"),
Value: awsv2.String("yes"),
Key: aws.String("teleport"),
Value: aws.String("yes"),
},
{
Key: awsv2.String("Name"),
Value: awsv2.String("Present"),
Key: aws.String("Name"),
Value: aws.String("Present"),
},
},
State: &ec2types.InstanceState{
@@ -172,20 +171,20 @@ func TestEC2Watcher(t *testing.T) {
},
}
presentOther := ec2types.Instance{
InstanceId: awsv2.String("instance-present-2"),
InstanceId: aws.String("instance-present-2"),
Tags: []ec2types.Tag{{
Key: awsv2.String("env"),
Value: awsv2.String("dev"),
Key: aws.String("env"),
Value: aws.String("dev"),
}},
State: &ec2types.InstanceState{
Name: ec2types.InstanceStateNameRunning,
},
}
presentForEICE := ec2types.Instance{
InstanceId: awsv2.String("instance-present-3"),
InstanceId: aws.String("instance-present-3"),
Tags: []ec2types.Tag{{
Key: awsv2.String("with-eice"),
Value: awsv2.String("please"),
Key: aws.String("with-eice"),
Value: aws.String("please"),
}},
State: &ec2types.InstanceState{
Name: ec2types.InstanceStateNameRunning,
@@ -199,23 +198,23 @@ func TestEC2Watcher(t *testing.T) {
presentOther,
presentForEICE,
{
InstanceId: awsv2.String("instance-absent"),
InstanceId: aws.String("instance-absent"),
Tags: []ec2types.Tag{{
Key: awsv2.String("env"),
Value: awsv2.String("prod"),
Key: aws.String("env"),
Value: aws.String("prod"),
}},
State: &ec2types.InstanceState{
Name: ec2types.InstanceStateNameRunning,
},
},
{
InstanceId: awsv2.String("instance-absent-3"),
InstanceId: aws.String("instance-absent-3"),
Tags: []ec2types.Tag{{
Key: awsv2.String("env"),
Value: awsv2.String("prod"),
Key: aws.String("env"),
Value: aws.String("prod"),
}, {
Key: awsv2.String("teleport"),
Value: awsv2.String("yes"),
Key: aws.String("teleport"),
Value: aws.String("yes"),
}},
State: &ec2types.InstanceState{
Name: ec2types.InstanceStateNamePending,
+30 -16
View File
@@ -19,9 +19,9 @@
package aws
import (
"bytes"
"context"
"crypto/sha1"
"crypto/sha256"
"encoding/hex"
"fmt"
"log/slog"
@@ -33,14 +33,12 @@ import (
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/aws/arn"
"github.com/aws/aws-sdk-go/aws/credentials"
v4 "github.com/aws/aws-sdk-go/aws/signer/v4"
v4 "github.com/aws/aws-sdk-go-v2/aws/signer/v4"
"github.com/gravitational/trace"
apievents "github.com/gravitational/teleport/api/types/events"
apiawsutils "github.com/gravitational/teleport/api/utils/aws"
"github.com/gravitational/teleport/lib/utils"
"github.com/gravitational/teleport/lib/utils/aws/migration"
)
const (
@@ -77,6 +75,11 @@ const (
// https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_iam-quotas.html
MaxRoleSessionNameLength = 64
// EmptyPayloadHash is the SHA-256 for an empty element (as in echo -n | sha256sum).
// PresignHTTP requires the hash of the body, but when there is no body we hash the empty string.
// https://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-header-based-auth.html
EmptyPayloadHash = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
iamServiceName = "iam"
)
@@ -195,7 +198,8 @@ func VerifyAWSSignature(req *http.Request, credProvider aws.CredentialsProvider)
return trace.Wrap(err)
}
reqCopy := req.Clone(context.Background())
ctx := context.Background()
reqCopy := req.Clone(ctx)
// Remove all the headers that are not present in awsCred.SignedHeaders.
filterHeaders(reqCopy, sigV4.SignedHeaders)
@@ -207,8 +211,13 @@ func VerifyAWSSignature(req *http.Request, credProvider aws.CredentialsProvider)
return trace.BadParameter("%s", err)
}
signer := NewSignerV2(credProvider, sigV4.Service)
_, err = signer.Sign(reqCopy, bytes.NewReader(payload), sigV4.Service, sigV4.Region, t)
creds, err := credProvider.Retrieve(ctx)
if err != nil {
return trace.Wrap(err)
}
signer := NewSigner(sigV4.Service)
err = signer.SignHTTP(ctx, creds, reqCopy, GetV4PayloadHash(payload), sigV4.Service, sigV4.Region, t)
if err != nil {
return trace.Wrap(err)
}
@@ -226,23 +235,28 @@ func VerifyAWSSignature(req *http.Request, credProvider aws.CredentialsProvider)
return nil
}
// NewSignerV2 is a temporary AWS SDK migration helper.
func NewSignerV2(provider aws.CredentialsProvider, signingServiceName string) *v4.Signer {
return NewSigner(migration.NewCredentialsAdapter(provider), signingServiceName)
}
// NewSigner creates a new V4 signer.
func NewSigner(credentials *credentials.Credentials, signingServiceName string) *v4.Signer {
options := func(s *v4.Signer) {
func NewSigner(signingServiceName string) *v4.Signer {
return v4.NewSigner(func(opts *v4.SignerOptions) {
// s3 and s3control requests are signed with URL unescaped (found by
// searching "DisableURIPathEscaping" in "aws-sdk-go/service"). Both
// services use "s3" as signing name. See description of
// "DisableURIPathEscaping" for more details.
if signingServiceName == "s3" {
s.DisableURIPathEscaping = true
opts.DisableURIPathEscaping = true
}
})
}
// GetV4PayloadHash returns the V4 signing payload hash.
func GetV4PayloadHash(payload []byte) string {
if len(payload) == 0 {
return EmptyPayloadHash
}
return v4.NewSigner(credentials, options)
hash := sha256.New()
hash.Write(payload)
return hex.EncodeToString(hash.Sum(nil))
}
// filterHeaders removes request headers that are not in the headers list and returns the removed header keys.
-71
View File
@@ -1,71 +0,0 @@
/*
* Teleport
* Copyright (C) 2023 Gravitational, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package aws
import (
"context"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/endpoints"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/gravitational/trace"
"github.com/gravitational/teleport/lib/modules"
)
// AWSSessionProvider defines a function that creates an AWS Session.
// It must use ambient credentials if Integration is empty.
// It must use Integration credentials otherwise.
type AWSSessionProvider func(ctx context.Context, region string, integration string) (*session.Session, error)
// StaticAWSSessionProvider is a helper method that returns a static session.
// Must not be used to provide sessions when using Integrations.
func StaticAWSSessionProvider(awsSession *session.Session) AWSSessionProvider {
return func(ctx context.Context, region, integration string) (*session.Session, error) {
if integration != "" {
return nil, trace.BadParameter("integration %q is not allowed to use static sessions", integration)
}
return awsSession, nil
}
}
// SessionProviderUsingAmbientCredentials returns an AWS Session using ambient credentials.
// This is in contrast with AWS Sessions that can be generated using an AWS OIDC Integration.
func SessionProviderUsingAmbientCredentials() AWSSessionProvider {
return func(ctx context.Context, region, integration string) (*session.Session, error) {
if integration != "" {
return nil, trace.BadParameter("integration %q is not allowed to use ambient sessions", integration)
}
useFIPSEndpoint := endpoints.FIPSEndpointStateUnset
if modules.GetModules().IsBoringBinary() {
useFIPSEndpoint = endpoints.FIPSEndpointStateEnabled
}
session, err := session.NewSessionWithOptions(session.Options{
SharedConfigState: session.SharedConfigEnable,
Config: aws.Config{
UseFIPSEndpoint: useFIPSEndpoint,
},
})
if err != nil {
return nil, trace.Wrap(err)
}
return session, nil
}
}
-22
View File
@@ -22,15 +22,12 @@ import (
"context"
"errors"
"io"
"net/http"
"strings"
awsv2 "github.com/aws/aws-sdk-go-v2/aws"
managerv2 "github.com/aws/aws-sdk-go-v2/feature/s3/manager"
s3v2 "github.com/aws/aws-sdk-go-v2/service/s3"
s3types "github.com/aws/aws-sdk-go-v2/service/s3/types"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/service/s3"
"github.com/aws/smithy-go"
"github.com/gravitational/trace"
)
@@ -42,25 +39,6 @@ func ConvertS3Error(err error) error {
return nil
}
// SDK v1 errors:
var rerr awserr.RequestFailure
if errors.As(err, &rerr) && rerr.StatusCode() == http.StatusForbidden {
return trace.AccessDenied("%s", rerr.Message())
}
var aerr awserr.Error
if errors.As(err, &aerr) {
switch aerr.Code() {
case s3.ErrCodeNoSuchKey, s3.ErrCodeNoSuchBucket, s3.ErrCodeNoSuchUpload, "NotFound":
return trace.NotFound("%s", aerr)
case s3.ErrCodeBucketAlreadyExists, s3.ErrCodeBucketAlreadyOwnedByYou:
return trace.AlreadyExists("%s", aerr)
default:
return trace.BadParameter("%s", aerr)
}
}
// SDK v2 errors:
var noSuchKey *s3types.NoSuchKey
if errors.As(err, &noSuchKey) {
return trace.NotFound("%s", noSuchKey)
+9 -3
View File
@@ -19,10 +19,10 @@
package aws
import (
"bytes"
"context"
"io"
"net/http"
"time"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/gravitational/trace"
@@ -94,8 +94,14 @@ func SignRequest(ctx context.Context, req *http.Request, signCtx *SigningCtx) (*
// 100-continue" headers without being signed, otherwise the Athena service
// would reject the requests.
unsignedHeaders := removeUnsignedHeaders(reqCopy)
signer := NewSignerV2(signCtx.Credentials, signCtx.SigningName)
_, err = signer.Sign(reqCopy, bytes.NewReader(payload), signCtx.SigningName, signCtx.SigningRegion, signCtx.Clock.Now())
creds, err := signCtx.Credentials.Retrieve(ctx)
if err != nil {
return nil, trace.Wrap(err)
}
signer := NewSigner(signCtx.SigningName)
err = signer.SignHTTP(ctx, creds, reqCopy, GetV4PayloadHash(payload), signCtx.SigningName, signCtx.SigningRegion, time.Now())
if err != nil {
return nil, trace.Wrap(err)
}
-83
View File
@@ -1,83 +0,0 @@
/*
* Teleport
* Copyright (C) 2023 Gravitational, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package aws
import (
"bytes"
"encoding/xml"
"github.com/aws/aws-sdk-go/private/protocol/xml/xmlutil"
"github.com/gravitational/trace"
)
// IsXMLOfLocalName returns true if the root XML has the provided (local) name.
func IsXMLOfLocalName(data []byte, wantLocalName string) bool {
var name xml.Name
if err := xml.Unmarshal(data, &name); err == nil {
return wantLocalName == name.Local
}
return false
}
// UnmarshalXMLChildNode decodes the XML-encoded data and stores the child node
// with the specified name to v, where v is a pointer to an AWS SDK v1 struct.
func UnmarshalXMLChildNode(v interface{}, data []byte, childName string) error {
return trace.Wrap(xmlutil.UnmarshalXML(v, xml.NewDecoder(bytes.NewReader(data)), childName))
}
// MarshalXML marshals the provided root name and a map of children in XML with
// default indent (prefix "", indent " ").
func MarshalXML(rootName xml.Name, children map[string]any) ([]byte, error) {
var buf bytes.Buffer
encoder := xml.NewEncoder(&buf)
encoder.Indent("", " ")
err := encodeXMLNode(encoder, rootName, func() error {
for childName, childValue := range children {
if err := encodeXMLNodeAWSSDKV1(encoder, childName, childValue); err != nil {
return trace.Wrap(err)
}
}
return nil
})
if err != nil {
return nil, trace.Wrap(err)
}
if err := trace.Wrap(encoder.Flush()); err != nil {
return nil, trace.Wrap(err)
}
return buf.Bytes(), nil
}
func encodeXMLNode(encoder *xml.Encoder, name xml.Name, encodeChildren func() error) error {
startElement := xml.StartElement{Name: name}
if err := encoder.EncodeToken(startElement); err != nil {
return trace.Wrap(err)
}
if err := encodeChildren(); err != nil {
return trace.Wrap(err)
}
return trace.Wrap(encoder.EncodeToken(startElement.End()))
}
func encodeXMLNodeAWSSDKV1(encoder *xml.Encoder, name string, v any) error {
return encodeXMLNode(encoder, xml.Name{Local: name}, func() error {
return trace.Wrap(xmlutil.BuildXML(v, encoder))
})
}
-123
View File
@@ -1,123 +0,0 @@
/*
* Teleport
* Copyright (C) 2023 Gravitational, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package aws
import (
"encoding/xml"
"net/http"
"strings"
"testing"
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/private/protocol"
"github.com/aws/aws-sdk-go/service/sts"
"github.com/stretchr/testify/require"
)
func TestIsXMLOfLocalName(t *testing.T) {
data := `<MyXMLName xmlns="my-space"><MyNode><Value>5</Value></MyNode></MyXMLName>`
require.True(t, IsXMLOfLocalName([]byte(data), "MyXMLName"))
require.False(t, IsXMLOfLocalName([]byte(data), "SomeOtherName"))
require.False(t, IsXMLOfLocalName([]byte("<bad-xml"+data), "MyXMLName"))
}
func TestUnmarshalXMLChildNode(t *testing.T) {
want := sts.AssumeRoleOutput{
AssumedRoleUser: &sts.AssumedRoleUser{
Arn: aws.String("some-arn"),
},
Credentials: &sts.Credentials{
AccessKeyId: aws.String("some-access-key-id"),
SecretAccessKey: aws.String("some-secret-access-key"),
SessionToken: aws.String("some-session-token"),
Expiration: aws.Time(time.Unix(1234567890, 0).UTC()),
},
}
body := []byte(`<AssumeRoleResponse xmlns="https://sts.amazonaws.com/doc/2011-06-15/">
<AssumeRoleResult>
<Credentials>
<SecretAccessKey>some-secret-access-key</SecretAccessKey>
<SessionToken>some-session-token</SessionToken>
<AccessKeyId>some-access-key-id</AccessKeyId>
<Expiration>2009-02-13T23:31:30Z</Expiration>
</Credentials>
<AssumedRoleUser>
<Arn>some-arn</Arn>
</AssumedRoleUser>
</AssumeRoleResult>
<ResponseMetadata>
<StatusCode>200</StatusCode>
<RequestID>some-request-id</RequestID>
</ResponseMetadata>
</AssumeRoleResponse>`)
var actual sts.AssumeRoleOutput
require.NoError(t, UnmarshalXMLChildNode(&actual, body, "AssumeRoleResult"))
require.Equal(t, want, actual)
}
func TestMarshalXMLIndent(t *testing.T) {
data, err := MarshalXML(
xml.Name{
Local: "AssumeRoleResponse",
Space: "https://sts.amazonaws.com/doc/2011-06-15/",
},
map[string]any{
"AssumeRoleResult": sts.AssumeRoleOutput{
AssumedRoleUser: &sts.AssumedRoleUser{
Arn: aws.String("some-arn"),
},
Credentials: &sts.Credentials{
AccessKeyId: aws.String("some-access-key-id"),
SecretAccessKey: aws.String("some-secret-access-key"),
SessionToken: aws.String("some-session-token"),
Expiration: aws.Time(time.Unix(1234567890, 0).UTC()),
},
},
"ResponseMetadata": protocol.ResponseMetadata{
RequestID: "some-request-id",
StatusCode: http.StatusOK,
},
},
)
require.NoError(t, err)
// Nodes are not sorted. Use ElementsMatch to ensure each line is present.
require.ElementsMatch(t, []string{
`<AssumeRoleResponse xmlns="https://sts.amazonaws.com/doc/2011-06-15/">`,
` <AssumeRoleResult>`,
` <Credentials>`,
` <SecretAccessKey>some-secret-access-key</SecretAccessKey>`,
` <SessionToken>some-session-token</SessionToken>`,
` <AccessKeyId>some-access-key-id</AccessKeyId>`,
` <Expiration>2009-02-13T23:31:30Z</Expiration>`,
` </Credentials>`,
` <AssumedRoleUser>`,
` <Arn>some-arn</Arn>`,
` </AssumedRoleUser>`,
` </AssumeRoleResult>`,
` <ResponseMetadata>`,
` <StatusCode>200</StatusCode>`,
` <RequestID>some-request-id</RequestID>`,
` </ResponseMetadata>`,
`</AssumeRoleResponse>`,
}, strings.Split(string(data), "\n"))
}