// Copyright IBM Corp. 2021, 2026 // SPDX-License-Identifier: MPL-2.0 package errors import ( "bytes" "errors" "fmt" "net/http" "sort" "strings" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" "github.com/aws/aws-sdk-go-v2/aws/retry" awshttp "github.com/aws/aws-sdk-go-v2/aws/transport/http" "github.com/aws/aws-sdk-go-v2/service/s3/types" "github.com/aws/smithy-go" pb "github.com/hashicorp/boundary/sdk/pbs/plugin" "google.golang.org/protobuf/types/known/timestamppb" ) const ( // awsErrorAccessDenied is returned when the credentials do not have // the required permissions to do a specific action on a resource. // This error will persist until the IAM Role Policy is updated by // the owner. This error cannot be resolved by retrying the request. awsErrorAccessDenied = "AccessDenied" // awsErrorUnauthorizedOperation is returned by EC2 when the caller is not // authorized to perform the operation. This includes DryRun permission // checks that fail (the counterpart to DryRunOperation success). awsErrorUnauthorizedOperation = "UnauthorizedOperation" // awsErrorInvalidAccessKeyId is returned when the credential does // not exist. This error will persist until the credentials attached // to the storage bucket is updated. This error can occur when the // given credentials are deleted, deactivated, or rotated. This // error cannot be resolved by retrying the request. awsErrorInvalidAccessKeyId = "InvalidAccessKeyId" // awsErrorExpiredToken is returned when the provided credential is // expired. This error can only be returned by a dynamic credential // type. This error can resolve itself by retrying the request. awsErrorExpiredToken = "ExpiredToken" // NoSuchBucket is returned when the specified S3 storage bucket does // not exist awsErrorNoSuchBucket = "NoSuchBucket" // BadDigest is returned when an S3 PUT OBJECT request's calculated // checksum does not match the checksum which was sent by the client awsErrorBadDigest = "BadDigest" // NoSuchKey is returned when attempting to interact with an S3 object // whose key does not exist awsErrorNoSuchKey = "NoSuchKey" // NotFound is returned when attempting to interact with an S3 object // whose key does not exist awsErrorNotFound = "NotFound" // The InvalidObjectState error is returned when trying to // access an object that was moved into cold storage. awsErrorInvalidObjectState = "InvalidObjectState" // RequestTimeout is returned when an http request takes longer than allowed awsErrorRequestTimeout = "RequestTimeout" // RequestTimeoutException is returned when an http request takes longer than allowed awsErrorRequestTimeoutException = "RequestTimeoutException" // dryRunOperationErrorCode is returned by AWS when a DryRun request would have // succeeded. It is a success signal disguised as an API error (typically HTTP 412). dryRunOperationErrorCode = "DryRunOperation" ) // IsDryRunSuccess reports whether err is the AWS DryRunOperation success signal. // A true result means the dry-run validated credentials, permissions, and parameters; // it must not be treated as a failure. func IsDryRunSuccess(err error) bool { if err == nil { return false } var apiErr smithy.APIError if !errors.As(err, &apiErr) { return false } return apiErr.ErrorCode() == dryRunOperationErrorCode } // InvalidArgumentError returns an grpc invalid argument status error. func InvalidArgumentError(msg string, f map[string]string) error { var fieldMsgs []string for field, val := range f { fieldMsgs = append(fieldMsgs, fmt.Sprintf("%s: %s", field, val)) } if len(fieldMsgs) > 0 { sort.Strings(fieldMsgs) msg = fmt.Sprintf("%s: [%s]", msg, strings.Join(fieldMsgs, ", ")) } return BadRequestStatus(msg) } // ParseAWSError converts an aws service error into a RPC status. This method // will handle parsing the following aws error codes: throttling, connections, // and credentials. This method will fallback to parsing the http status code // when it cannot match an aws error code. This method does not handle specific // service type errors such as S3 or EC2. func ParseAWSError(op string, err error) (st *status.Status, permission *pb.Permission) { if err == nil { return nil, nil } msg := fmt.Sprintf("%s: %v", op, err) // find the service name of the aws api serviceName := "unknown" var oe *smithy.OperationError if errors.As(err, &oe) { serviceName = oe.Service() } // The unknown plugin error code will be used // when the aws service returns an error that // has failed to be parsed. By default, this // error code is retryable. defer func() { if st == nil { statusMsg := fmt.Sprintf("aws service %s: unknown error: %s", serviceName, msg) st = status.New(codes.Unknown, statusMsg) } if permission == nil { permission = &pb.Permission{State: pb.StateType_STATE_TYPE_UNKNOWN, CheckedAt: timestamppb.Now()} } }() // evaluate against aws throttling error codes throttleErr := retry.ThrottleErrorCode{ Codes: retry.DefaultThrottleErrorCodes, } if throttleErr.IsErrorThrottle(err).Bool() { statusMsg := fmt.Sprintf("aws service %s: throttling: %s", serviceName, msg) return status.New(codes.Unavailable, statusMsg), nil } // match error type against NotFound var notFoundErr *types.NotFound if errors.As(err, ¬FoundErr) { statusMsg := fmt.Sprintf("aws service %s: %s", serviceName, msg) return status.New(codes.NotFound, statusMsg), nil } // match error type against NoSuchkey var noSuchKeyErr *types.NoSuchKey if errors.As(err, &noSuchKeyErr) { statusMsg := fmt.Sprintf("aws service %s: %s", serviceName, msg) return status.New(codes.NotFound, statusMsg), nil } // match error type against InvalidObjectState var invalidObjectStateErr *types.InvalidObjectState if errors.As(err, &invalidObjectStateErr) { statusMsg := fmt.Sprintf("aws service %s: %s", serviceName, msg) return status.New(codes.NotFound, statusMsg), nil } // match error type against NoSuchBucket var noSuchBucketErr *types.NoSuchBucket if errors.As(err, &noSuchBucketErr) { statusMsg := fmt.Sprintf("aws service %s: %s", serviceName, msg) return status.New(codes.NotFound, statusMsg), &pb.Permission{ State: pb.StateType_STATE_TYPE_ERROR, ErrorDetails: noSuchBucketErr.ErrorMessage(), CheckedAt: timestamppb.Now(), } } // match error type against AccessDenied var accessDeniedErr *types.AccessDenied if errors.As(err, &accessDeniedErr) { statusMsg := fmt.Sprintf("aws service %s: invalid credentials: %s", serviceName, msg) return status.New(codes.PermissionDenied, statusMsg), &pb.Permission{ State: pb.StateType_STATE_TYPE_ERROR, ErrorDetails: accessDeniedErr.ErrorMessage(), CheckedAt: timestamppb.Now(), } } // fallback to evaluating the smithy APIError code var apiErr smithy.APIError if errors.As(err, &apiErr) { switch apiErr.ErrorCode() { case awsErrorAccessDenied: fallthrough case awsErrorUnauthorizedOperation: fallthrough case awsErrorInvalidAccessKeyId: fallthrough case awsErrorExpiredToken: statusMsg := fmt.Sprintf("aws service %s: invalid credentials: %s", serviceName, msg) return status.New(codes.PermissionDenied, statusMsg), &pb.Permission{ State: pb.StateType_STATE_TYPE_ERROR, ErrorDetails: apiErr.ErrorMessage(), CheckedAt: timestamppb.Now(), } case awsErrorNoSuchBucket: statusMsg := fmt.Sprintf("aws service %s: %s", serviceName, msg) return status.New(codes.NotFound, statusMsg), &pb.Permission{ State: pb.StateType_STATE_TYPE_ERROR, ErrorDetails: apiErr.ErrorMessage(), CheckedAt: timestamppb.Now(), } case awsErrorBadDigest: statusMsg := fmt.Sprintf("aws service %s: %s", serviceName, msg) return status.New(codes.Aborted, statusMsg), nil case awsErrorNoSuchKey: fallthrough case awsErrorNotFound: fallthrough case awsErrorInvalidObjectState: statusMsg := fmt.Sprintf("aws service %s: %s", serviceName, msg) return status.New(codes.NotFound, statusMsg), nil case awsErrorRequestTimeout: fallthrough case awsErrorRequestTimeoutException: statusMsg := fmt.Sprintf("aws service %s: %s", serviceName, msg) return status.New(codes.DeadlineExceeded, statusMsg), nil } } // fallback to evaluating the http status code var httpErr *awshttp.ResponseError if errors.As(err, &httpErr) { buf := new(bytes.Buffer) _, err = buf.ReadFrom(httpErr.Response.Body) if err != nil { return status.New(codes.Internal, err.Error()), nil } defer httpErr.Response.Body.Close() switch httpErr.HTTPStatusCode() { case http.StatusBadRequest: statusMsg := fmt.Sprintf("aws service %s: bad request: %s", serviceName, msg) return status.New(codes.InvalidArgument, statusMsg), nil case http.StatusUnauthorized: statusMsg := fmt.Sprintf("aws service %s: invalid credentials: %s", serviceName, msg) return status.New(codes.PermissionDenied, statusMsg), &pb.Permission{ State: pb.StateType_STATE_TYPE_ERROR, ErrorDetails: buf.String(), CheckedAt: timestamppb.Now(), } case http.StatusForbidden: statusMsg := fmt.Sprintf("aws service %s: invalid credentials: %s", serviceName, msg) return status.New(codes.PermissionDenied, statusMsg), &pb.Permission{ State: pb.StateType_STATE_TYPE_ERROR, ErrorDetails: buf.String(), CheckedAt: timestamppb.Now(), } case http.StatusNotFound: statusMsg := fmt.Sprintf("aws service %s: resource not found: %s", serviceName, msg) return status.New(codes.NotFound, statusMsg), nil case http.StatusTooManyRequests: statusMsg := fmt.Sprintf("aws service %s: throttling: %s", serviceName, msg) return status.New(codes.Unavailable, statusMsg), nil case http.StatusInternalServerError: fallthrough case http.StatusBadGateway: fallthrough case http.StatusServiceUnavailable: statusMsg := fmt.Sprintf("aws service %s: connectivity: %s", serviceName, msg) return status.New(codes.Unavailable, statusMsg), nil case http.StatusRequestTimeout: fallthrough case http.StatusGatewayTimeout: statusMsg := fmt.Sprintf("aws service %s: timeout: %s", serviceName, msg) return status.New(codes.DeadlineExceeded, statusMsg), nil } } return st, nil } // BadRequestStatusf returns a status error with an invalid // argument code and a formatted error message. func BadRequestStatusf(format string, args ...any) error { return status.New(codes.InvalidArgument, fmt.Sprintf(format, args...)).Err() } // BadRequestStatus returns a status error with an invalid // argument code and the provided message. func BadRequestStatus(msg string) error { return status.New(codes.InvalidArgument, msg).Err() } // UnknownStatusf returns a status error with an internal // error code and a formatted error message. func UnknownStatusf(format string, args ...any) error { return status.New(codes.Internal, fmt.Sprintf(format, args...)).Err() } // UnknownStatus returns a status error with an internal // error code and the provided message. func UnknownStatus(msg string) error { return status.New(codes.Internal, msg).Err() }