Files
threshold/internal/servers/controller/handlers/errors.go
T
Jeff Mitchell aa570f4e48 Honor child scope list permissions when recursing (#1016)
Current behavior is that you need list grant on the parent scope when
recursively listing, and this is sufficient. This is good for
administrative workflows but can be unintuitive for resources that only
exist at a project level. This change allows permissions on the child
scope to also be taken into account, so that you can start a recursive
list from a parent scope without direct list support on that scope but
still get back child scopes for which list access is granted.

This also fixes a bug where visibility of a scope for recursive listing
would be based on whether the user had list resources for roles within
that scope instead of for the resource type in question.
2021-03-24 15:33:04 -04:00

217 lines
6.6 KiB
Go

package handlers
import (
"context"
"fmt"
"io"
"net/http"
"sort"
"strings"
"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
"github.com/hashicorp/boundary/internal/errors"
pb "github.com/hashicorp/boundary/internal/gen/controller/api"
"github.com/hashicorp/go-hclog"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
const (
genericUniquenessMsg = "Invalid request. Request attempted to make second resource with the same field value that must be unique."
genericNotFoundMsg = "Unable to find requested resource."
)
type apiError struct {
status int32
inner *pb.Error
}
func (e *apiError) Error() string {
res := fmt.Sprintf("Status: %d, Kind: %q, Error: %q", e.status, e.inner.GetKind(), e.inner.GetMessage())
var dets []string
for _, rf := range e.inner.GetDetails().GetRequestFields() {
dets = append(dets, fmt.Sprintf("{name: %q, desc: %q}", rf.GetName(), rf.GetDescription()))
}
if len(dets) > 0 {
det := strings.Join(dets, ", ")
res = fmt.Sprintf("%s, Details: {%s}", res, det)
}
return res
}
func (e *apiError) Is(target error) bool {
var tApiErr *apiError
if !errors.As(target, &tApiErr) {
return false
}
return tApiErr.inner.Kind == e.inner.Kind && tApiErr.status == e.status
}
// ApiErrorWithCode returns an api error with the provided code.
func ApiErrorWithCode(c codes.Code) error {
return &apiError{
status: int32(runtime.HTTPStatusFromCode(c)),
inner: &pb.Error{
Kind: c.String(),
},
}
}
// ApiErrorWithCodeAndMessage returns an api error with the provided code and message.
func ApiErrorWithCodeAndMessage(c codes.Code, msg string, args ...interface{}) error {
return &apiError{
status: int32(runtime.HTTPStatusFromCode(c)),
inner: &pb.Error{
Kind: c.String(),
Message: fmt.Sprintf(msg, args...),
},
}
}
// NotFoundError returns an ApiError indicating a resource couldn't be found.
func NotFoundError() error {
return &apiError{
status: http.StatusNotFound,
inner: &pb.Error{
Kind: codes.NotFound.String(),
Message: "Resource not found.",
},
}
}
// NotFoundErrorf returns an ApiError indicating a resource couldn't be found.
func NotFoundErrorf(msg string, a ...interface{}) error {
return &apiError{
status: http.StatusNotFound,
inner: &pb.Error{
Kind: codes.NotFound.String(),
Message: fmt.Sprintf(msg, a...),
},
}
}
var unauthorizedError = &apiError{
status: http.StatusForbidden,
inner: &pb.Error{
Kind: codes.PermissionDenied.String(),
Message: "Forbidden.",
},
}
func ForbiddenError() error {
return unauthorizedError
}
var unauthenticatedError = &apiError{
status: http.StatusUnauthorized,
inner: &pb.Error{
Kind: codes.Unauthenticated.String(),
Message: "Unauthenticated, or invalid token.",
},
}
func UnauthenticatedError() error {
return unauthenticatedError
}
func InvalidArgumentErrorf(msg string, fields map[string]string) error {
err := ApiErrorWithCodeAndMessage(codes.InvalidArgument, msg)
var apiErr *apiError
if !errors.As(err, &apiErr) {
hclog.L().Error("Unable to build invalid argument api error.", "original error", err)
}
if len(fields) > 0 {
apiErr.inner.Details = &pb.ErrorDetails{}
}
for k, v := range fields {
apiErr.inner.Details.RequestFields = append(apiErr.inner.Details.RequestFields, &pb.FieldError{Name: k, Description: v})
}
sort.Slice(apiErr.inner.GetDetails().GetRequestFields(), func(i, j int) bool {
return apiErr.inner.Details.RequestFields[i].GetName() < apiErr.inner.Details.RequestFields[j].GetName()
})
return apiErr
}
// Converts a known errors into an error that can presented to an end user over the API.
func backendErrorToApiError(inErr error) error {
stErr := status.Convert(inErr)
switch {
case errors.Is(inErr, runtime.ErrNotMatch):
// grpc gateway uses this error when the path was not matched, but the error uses codes.Unimplemented which doesn't match the intention.
// Overwrite the error to match our expected behavior.
return &apiError{
status: http.StatusNotFound,
inner: &pb.Error{
Kind: codes.NotFound.String(),
Message: http.StatusText(http.StatusNotFound),
},
}
case status.Code(inErr) == codes.Unimplemented:
// Instead of returning a 501 we always want to return a 405 when a method isn't implemented.
return &apiError{
status: http.StatusMethodNotAllowed,
inner: &pb.Error{
Kind: codes.Unimplemented.String(),
Message: stErr.Message(),
},
}
case errors.Match(errors.T(errors.RecordNotFound), inErr):
return NotFoundErrorf(genericNotFoundMsg)
case errors.Match(errors.T(errors.InvalidFieldMask), inErr), errors.Match(errors.T(errors.EmptyFieldMask), inErr):
return InvalidArgumentErrorf("Error in provided request", map[string]string{"update_mask": "Invalid update mask provided."})
case errors.IsUniqueError(inErr):
return InvalidArgumentErrorf(genericUniquenessMsg, nil)
}
var statusCode int32 = http.StatusInternalServerError
var domainErr *errors.Err
if errors.As(inErr, &domainErr) && domainErr.Code >= 400 && domainErr.Code <= 599 {
// Domain error codes 400-599 align with http client and server error codes, use the domain error code instead of 500
statusCode = int32(domainErr.Code)
}
// TODO: Don't return potentially sensitive information (like which user id an account
// is already associated with when attempting to re-associate it).
return &apiError{
status: statusCode,
inner: &pb.Error{Kind: codes.Internal.String(), Message: inErr.Error()},
}
}
func ErrorHandler(logger hclog.Logger) runtime.ErrorHandlerFunc {
const errorFallback = `{"error": "failed to marshal error message"}`
return func(ctx context.Context, _ *runtime.ServeMux, mar runtime.Marshaler, w http.ResponseWriter, r *http.Request, inErr error) {
// API specified error, otherwise we need to translate repo/db errors.
var apiErr *apiError
isApiErr := errors.As(inErr, &apiErr)
if !isApiErr {
if err := backendErrorToApiError(inErr); err != nil && !errors.As(err, &apiErr) {
logger.Error("failed to cast error to api error", "error", err)
}
}
if apiErr.status == http.StatusInternalServerError {
logger.Error("internal error returned", "error", inErr)
}
buf, merr := mar.Marshal(apiErr.inner)
if merr != nil {
logger.Error("failed to marshal error response", "response", fmt.Sprintf("%#v", apiErr.inner), "error", merr)
w.WriteHeader(http.StatusInternalServerError)
if _, err := io.WriteString(w, errorFallback); err != nil {
logger.Error("failed to write response", "error", err)
}
return
}
w.Header().Set("Content-Type", mar.ContentType(apiErr.inner))
w.WriteHeader(int(apiErr.status))
if _, err := w.Write(buf); err != nil {
logger.Error("failed to send response chunk", "error", err)
return
}
}
}