Fork of hashicorp/boundary-plugin-aws (MPL-2.0). Module path and Boundary SDK imports now point at repo.anhonesthost.net/CyberCoveLLC. Adds NOTICE and an About this fork section to the README. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
308 lines
11 KiB
Go
308 lines
11 KiB
Go
// 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 "repo.anhonesthost.net/CyberCoveLLC/threshold/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()
|
|
}
|