1package customizations
2
3import (
4	"context"
5	"fmt"
6	"net/url"
7	"strings"
8
9	"github.com/aws/smithy-go/middleware"
10	smithyhttp "github.com/aws/smithy-go/transport/http"
11
12	"github.com/aws/aws-sdk-go-v2/aws"
13	awsarn "github.com/aws/aws-sdk-go-v2/aws/arn"
14	awsmiddleware "github.com/aws/aws-sdk-go-v2/aws/middleware"
15	"github.com/aws/aws-sdk-go-v2/service/internal/s3shared"
16	"github.com/aws/aws-sdk-go-v2/service/internal/s3shared/arn"
17	s3endpoints "github.com/aws/aws-sdk-go-v2/service/s3control/internal/endpoints/s3"
18)
19
20const (
21	// outpost id header
22	outpostIDHeader = "x-amz-outpost-id"
23
24	// account id header
25	accountIDHeader = "x-amz-account-id"
26)
27
28// processARNResource is used to process an ARN resource.
29type processARNResource struct {
30
31	// CopyInput creates a copy of input to be modified, this ensures the original input is not modified.
32	CopyInput func(interface{}) (interface{}, error)
33
34	// UpdateARNField points to a function that takes in a copy of input, updates the ARN field with
35	// the provided value and returns the input
36	UpdateARNField func(interface{}, string) error
37
38	// UseARNRegion indicates if region parsed from an ARN should be used.
39	UseARNRegion bool
40
41	// UseDualstack instructs if s3 dualstack endpoint config is enabled
42	UseDualstack bool
43
44	// EndpointResolver used to resolve endpoints. This may be a custom endpoint resolver
45	EndpointResolver EndpointResolver
46
47	// EndpointResolverOptions used by endpoint resolver
48	EndpointResolverOptions EndpointResolverOptions
49}
50
51// ID returns the middleware ID.
52func (*processARNResource) ID() string { return "S3Control:ProcessARNResourceMiddleware" }
53
54func (m *processARNResource) HandleSerialize(
55	ctx context.Context, in middleware.SerializeInput, next middleware.SerializeHandler,
56) (
57	out middleware.SerializeOutput, metadata middleware.Metadata, err error,
58) {
59	// if arn region resolves to custom endpoint that is mutable
60	if smithyhttp.GetHostnameImmutable(ctx) {
61		return next.HandleSerialize(ctx, in)
62	}
63
64	// check if arn was provided, if not skip this middleware
65	arnValue, ok := s3shared.GetARNResourceFromContext(ctx)
66	if !ok {
67		return next.HandleSerialize(ctx, in)
68	}
69
70	req, ok := in.Request.(*smithyhttp.Request)
71	if !ok {
72		return out, metadata, fmt.Errorf("unknown request type %T", req)
73	}
74
75	// parse arn into an endpoint arn wrt to service
76	resource, err := parseEndpointARN(arnValue)
77	if err != nil {
78		return out, metadata, err
79	}
80
81	resourceRequest := s3shared.ResourceRequest{
82		Resource:      resource,
83		RequestRegion: awsmiddleware.GetRegion(ctx),
84		SigningRegion: awsmiddleware.GetSigningRegion(ctx),
85		PartitionID:   awsmiddleware.GetPartitionID(ctx),
86		UseARNRegion:  m.UseARNRegion,
87	}
88
89	// validate resource request
90	if err := validateResourceRequest(resourceRequest); err != nil {
91		return out, metadata, err
92	}
93
94	// if not done already, clone the input and reassign it to in.Parameters
95	if !s3shared.IsClonedInput(ctx) {
96		in.Parameters, err = m.CopyInput(in.Parameters)
97		if err != nil {
98			return out, metadata, fmt.Errorf("error creating a copy of input while processing arn")
99		}
100		// set copy input key on context
101		ctx = s3shared.SetClonedInputKey(ctx, true)
102	}
103
104	// switch to correct endpoint updater
105	switch tv := resource.(type) {
106	case arn.OutpostAccessPointARN:
107		// validations
108		// check if dual stack
109		if m.UseDualstack {
110			return out, metadata, s3shared.NewClientConfiguredForDualStackError(tv,
111				resourceRequest.PartitionID, resourceRequest.RequestRegion, nil)
112		}
113
114		// check if resource arn region is FIPS
115		if resourceRequest.ResourceConfiguredForFIPS() {
116			return out, metadata, s3shared.NewInvalidARNWithFIPSError(tv, nil)
117		}
118
119		// Disable endpoint host prefix for s3-control
120		ctx = smithyhttp.DisableEndpointHostPrefix(ctx, true)
121
122		if m.UpdateARNField == nil {
123			return out, metadata, fmt.Errorf("error updating arnable field while serializing")
124		}
125
126		// update the arnable field with access point name
127		err = m.UpdateARNField(in.Parameters, tv.AccessPointName)
128		if err != nil {
129			return out, metadata, fmt.Errorf("error updating arnable field while serializing")
130		}
131
132		// check if request region is FIPS and ARN region usage is not allowed
133		if resourceRequest.UseFips() && !m.UseARNRegion {
134			return out, metadata, s3shared.NewInvalidARNWithFIPSError(tv, nil)
135		}
136
137		// Add outpostID header
138		req.Header.Add(outpostIDHeader, tv.OutpostID)
139
140		// build outpost access point request
141		ctx, err = buildOutpostAccessPointRequest(ctx, outpostAccessPointOptions{
142			processARNResource: *m,
143			request:            req,
144			resource:           tv,
145			partitionID:        resourceRequest.PartitionID,
146			requestRegion:      resourceRequest.RequestRegion,
147		})
148		if err != nil {
149			return out, metadata, err
150		}
151
152	// process outpost accesspoint ARN
153	case arn.OutpostBucketARN:
154		// check if dual stack
155		if m.UseDualstack {
156			return out, metadata, s3shared.NewClientConfiguredForDualStackError(tv,
157				resourceRequest.PartitionID, resourceRequest.RequestRegion, nil)
158		}
159
160		// check if resource arn region is FIPS
161		if resourceRequest.ResourceConfiguredForFIPS() {
162			return out, metadata, s3shared.NewInvalidARNWithFIPSError(tv, nil)
163		}
164
165		// Disable endpoint host prefix for s3-control
166		ctx = smithyhttp.DisableEndpointHostPrefix(ctx, true)
167
168		if m.UpdateARNField == nil {
169			return out, metadata, fmt.Errorf("error updating arnable field while serializing")
170		}
171
172		// update the arnable field with bucket name
173		err = m.UpdateARNField(in.Parameters, tv.BucketName)
174		if err != nil {
175			return out, metadata, fmt.Errorf("error updating arnable field while serializing")
176		}
177
178		// Add outpostID header
179		req.Header.Add(outpostIDHeader, tv.OutpostID)
180
181		// build outpost bucket request
182		ctx, err = buildOutpostBucketRequest(ctx, outpostBucketOptions{
183			processARNResource: *m,
184			request:            req,
185			resource:           tv,
186			partitionID:        resourceRequest.PartitionID,
187			requestRegion:      resourceRequest.RequestRegion,
188		})
189		if err != nil {
190			return out, metadata, err
191		}
192
193	default:
194		return out, metadata, s3shared.NewInvalidARNError(resource, nil)
195	}
196
197	// Add account-id header for the request if not present.
198	// SDK must always send the x-amz-account-id header for all requests
199	// where an accountId has been extracted from an ARN or the accountId field modeled as a header.
200	if h := req.Header.Get(accountIDHeader); len(h) == 0 {
201		req.Header.Add(accountIDHeader, resource.GetARN().AccountID)
202	}
203
204	return next.HandleSerialize(ctx, in)
205}
206
207// validate if s3 resource and request config is compatible.
208func validateResourceRequest(resourceRequest s3shared.ResourceRequest) error {
209	// check if resourceRequest leads to a cross partition error
210	v, err := resourceRequest.IsCrossPartition()
211	if err != nil {
212		return err
213	}
214	if v {
215		// if cross partition
216		return s3shared.NewClientPartitionMismatchError(resourceRequest.Resource,
217			resourceRequest.PartitionID, resourceRequest.RequestRegion, nil)
218	}
219
220	// check if resourceRequest leads to a cross region error
221	if !resourceRequest.AllowCrossRegion() && resourceRequest.IsCrossRegion() {
222		// if cross region, but not use ARN region is not enabled
223		return s3shared.NewClientRegionMismatchError(resourceRequest.Resource,
224			resourceRequest.PartitionID, resourceRequest.RequestRegion, nil)
225	}
226
227	// resource configured with FIPS as region is not supported by outposts
228	if resourceRequest.ResourceConfiguredForFIPS() {
229		return s3shared.NewInvalidARNWithFIPSError(resourceRequest.Resource, nil)
230	}
231
232	return nil
233}
234
235// Used by shapes with members decorated as endpoint ARN.
236func parseEndpointARN(v awsarn.ARN) (arn.Resource, error) {
237	return arn.ParseResource(v, resourceParser)
238}
239
240func resourceParser(a awsarn.ARN) (arn.Resource, error) {
241	resParts := arn.SplitResource(a.Resource)
242	switch resParts[0] {
243	case "outpost":
244		return arn.ParseOutpostARNResource(a, resParts[1:])
245	default:
246		return nil, arn.InvalidARNError{ARN: a, Reason: "unknown resource type"}
247	}
248}
249
250// ====== Outpost Accesspoint ========
251
252type outpostAccessPointOptions struct {
253	processARNResource
254	request       *smithyhttp.Request
255	resource      arn.OutpostAccessPointARN
256	partitionID   string
257	requestRegion string
258}
259
260func buildOutpostAccessPointRequest(ctx context.Context, options outpostAccessPointOptions) (context.Context, error) {
261	tv := options.resource
262	req := options.request
263
264	// Build outpost access point resource
265	resolveRegion := tv.Region
266	resolveService := tv.Service
267
268	endpointsID := resolveService
269	if resolveService == "s3-outposts" {
270		endpointsID = "s3"
271	}
272
273	// resolve regional endpoint for resolved region.
274	var endpoint aws.Endpoint
275	var err error
276	endpointSource := awsmiddleware.GetEndpointSource(ctx)
277	if endpointsID == "s3" && endpointSource == aws.EndpointSourceServiceMetadata {
278		// use s3 endpoint resolver
279		endpoint, err = s3endpoints.New().ResolveEndpoint(resolveRegion, s3endpoints.Options{
280			DisableHTTPS: options.EndpointResolverOptions.DisableHTTPS,
281		})
282	} else {
283		endpoint, err = options.EndpointResolver.ResolveEndpoint(resolveRegion, options.EndpointResolverOptions)
284	}
285
286	if err != nil {
287		return ctx, s3shared.NewFailedToResolveEndpointError(
288			tv,
289			options.partitionID,
290			options.requestRegion,
291			err,
292		)
293	}
294
295	req.URL, err = url.Parse(endpoint.URL)
296	if err != nil {
297		return ctx, fmt.Errorf("failed to parse endpoint URL: %w", err)
298	}
299
300	// redirect signer to use resolved endpoint signing name and region
301	if len(endpoint.SigningName) != 0 {
302		ctx = awsmiddleware.SetSigningName(ctx, endpoint.SigningName)
303	} else {
304		// assign resolved service from arn as signing name
305		ctx = awsmiddleware.SetSigningName(ctx, resolveService)
306	}
307
308	if len(endpoint.SigningRegion) != 0 {
309		// redirect signer to use resolved endpoint signing name and region
310		ctx = awsmiddleware.SetSigningRegion(ctx, endpoint.SigningRegion)
311	} else {
312		ctx = awsmiddleware.SetSigningRegion(ctx, resolveRegion)
313	}
314
315	// skip arn processing, if arn region resolves to a immutable endpoint
316	if endpoint.HostnameImmutable {
317		return ctx, nil
318	}
319
320	// add url host as s3-outposts
321	cfgHost := req.URL.Host
322	if strings.HasPrefix(cfgHost, endpointsID) {
323		req.URL.Host = resolveService + cfgHost[len(endpointsID):]
324
325		// update serviceID to resolved service
326		ctx = awsmiddleware.SetServiceID(ctx, resolveService)
327	}
328
329	// validate the endpoint host
330	if err := smithyhttp.ValidateEndpointHost(req.URL.Host); err != nil {
331		return ctx, s3shared.NewInvalidARNError(tv, err)
332	}
333
334	// Disable endpoint host prefix for s3-control
335	ctx = smithyhttp.DisableEndpointHostPrefix(ctx, true)
336
337	return ctx, nil
338}
339
340// ======= Outpost Bucket =========
341type outpostBucketOptions struct {
342	processARNResource
343	request       *smithyhttp.Request
344	resource      arn.OutpostBucketARN
345	partitionID   string
346	requestRegion string
347}
348
349func buildOutpostBucketRequest(ctx context.Context, options outpostBucketOptions) (context.Context, error) {
350	tv := options.resource
351	req := options.request
352
353	// Build endpoint from outpost bucket arn
354	resolveRegion := tv.Region
355	resolveService := tv.Service
356	// Outpost bucket resource uses `s3-control` as serviceEndpointLabel
357	endpointsID := "s3-control"
358
359	// resolve regional endpoint for resolved region.
360	endpoint, err := options.EndpointResolver.ResolveEndpoint(resolveRegion, options.EndpointResolverOptions)
361	if err != nil {
362		return ctx, s3shared.NewFailedToResolveEndpointError(
363			tv,
364			options.partitionID,
365			options.requestRegion,
366			err,
367		)
368	}
369
370	// assign resolved endpoint url to request url
371	req.URL, err = url.Parse(endpoint.URL)
372	if err != nil {
373		return ctx, fmt.Errorf("failed to parse endpoint URL: %w", err)
374	}
375
376	if len(endpoint.SigningName) != 0 {
377		ctx = awsmiddleware.SetSigningName(ctx, endpoint.SigningName)
378	} else {
379		// assign resolved service from arn as signing name
380		ctx = awsmiddleware.SetSigningName(ctx, resolveService)
381	}
382
383	if len(endpoint.SigningRegion) != 0 {
384		// redirect signer to use resolved endpoint signing name and region
385		ctx = awsmiddleware.SetSigningRegion(ctx, endpoint.SigningRegion)
386	} else {
387		ctx = awsmiddleware.SetSigningRegion(ctx, resolveRegion)
388	}
389
390	// skip arn processing, if arn region resolves to a immutable endpoint
391	if endpoint.HostnameImmutable {
392		return ctx, nil
393	}
394
395	cfgHost := req.URL.Host
396	if strings.HasPrefix(cfgHost, endpointsID) {
397		// replace service endpointID label with resolved service
398		req.URL.Host = resolveService + cfgHost[len(endpointsID):]
399
400		// update serviceID to resolved service
401		ctx = awsmiddleware.SetServiceID(ctx, resolveService)
402	}
403
404	// validate the endpoint host
405	if err := smithyhttp.ValidateEndpointHost(req.URL.Host); err != nil {
406		return ctx, s3shared.NewInvalidARNError(tv, err)
407	}
408
409	// Disable endpoint host prefix for s3-control
410	ctx = smithyhttp.DisableEndpointHostPrefix(ctx, true)
411
412	return ctx, nil
413}
414