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