package admin import ( "encoding/json" "fmt" "log" "net/http" "os" "github.com/pkg/errors" "github.com/smallstep/certificates/errs" "github.com/smallstep/certificates/logging" ) // ProblemType is the type of the Admin problem. type ProblemType int const ( // ErrorNotFoundType resource not found. ErrorNotFoundType ProblemType = iota // ErrorAuthorityMismatchType resource Authority ID does not match the // context Authority ID. ErrorAuthorityMismatchType // ErrorDeletedType resource has been deleted. ErrorDeletedType // ErrorBadRequestType bad request. ErrorBadRequestType // ErrorNotImplementedType not implemented. ErrorNotImplementedType // ErrorUnauthorizedType internal server error. ErrorUnauthorizedType // ErrorServerInternalType internal server error. ErrorServerInternalType ) // String returns the string representation of the admin problem type, // fulfilling the Stringer interface. func (ap ProblemType) String() string { switch ap { case ErrorNotFoundType: return "notFound" case ErrorAuthorityMismatchType: return "authorityMismatch" case ErrorDeletedType: return "deleted" case ErrorBadRequestType: return "badRequest" case ErrorNotImplementedType: return "notImplemented" case ErrorUnauthorizedType: return "unauthorized" case ErrorServerInternalType: return "internalServerError" default: return fmt.Sprintf("unsupported error type '%d'", int(ap)) } } type errorMetadata struct { details string status int typ string String string } var ( errorServerInternalMetadata = errorMetadata{ typ: ErrorServerInternalType.String(), details: "the server experienced an internal error", status: 500, } errorMap = map[ProblemType]errorMetadata{ ErrorNotFoundType: { typ: ErrorNotFoundType.String(), details: "resource not found", status: http.StatusNotFound, }, ErrorAuthorityMismatchType: { typ: ErrorAuthorityMismatchType.String(), details: "resource not owned by authority", status: http.StatusUnauthorized, }, ErrorDeletedType: { typ: ErrorDeletedType.String(), details: "resource is deleted", status: http.StatusNotFound, }, ErrorNotImplementedType: { typ: ErrorNotImplementedType.String(), details: "not implemented", status: http.StatusNotImplemented, }, ErrorBadRequestType: { typ: ErrorBadRequestType.String(), details: "bad request", status: http.StatusBadRequest, }, ErrorUnauthorizedType: { typ: ErrorUnauthorizedType.String(), details: "unauthorized", status: http.StatusUnauthorized, }, ErrorServerInternalType: errorServerInternalMetadata, } ) // Error represents an Admin error type Error struct { Type string `json:"type"` Detail string `json:"detail"` Message string `json:"message"` Err error `json:"-"` Status int `json:"-"` } // IsType returns true if the error type matches the input type. func (e *Error) IsType(pt ProblemType) bool { return pt.String() == e.Type } // NewError creates a new Error type. func NewError(pt ProblemType, msg string, args ...interface{}) *Error { return newError(pt, errors.Errorf(msg, args...)) } func newError(pt ProblemType, err error) *Error { meta, ok := errorMap[pt] if !ok { meta = errorServerInternalMetadata return &Error{ Type: meta.typ, Detail: meta.details, Status: meta.status, Err: err, } } return &Error{ Type: meta.typ, Detail: meta.details, Status: meta.status, Err: err, } } // NewErrorISE creates a new ErrorServerInternalType Error. func NewErrorISE(msg string, args ...interface{}) *Error { return NewError(ErrorServerInternalType, msg, args...) } // WrapError attempts to wrap the internal error. func WrapError(typ ProblemType, err error, msg string, args ...interface{}) *Error { switch e := err.(type) { case nil: return nil case *Error: if e.Err == nil { e.Err = errors.Errorf(msg+"; "+e.Detail, args...) } else { e.Err = errors.Wrapf(e.Err, msg, args...) } return e default: return newError(typ, errors.Wrapf(err, msg, args...)) } } // WrapErrorISE shortcut to wrap an internal server error type. func WrapErrorISE(err error, msg string, args ...interface{}) *Error { return WrapError(ErrorServerInternalType, err, msg, args...) } // StatusCode returns the status code and implements the StatusCoder interface. func (e *Error) StatusCode() int { return e.Status } // Error allows AError to implement the error interface. func (e *Error) Error() string { return e.Err.Error() } // Cause returns the internal error and implements the Causer interface. func (e *Error) Cause() error { if e.Err == nil { return errors.New(e.Detail) } return e.Err } // ToLog implements the EnableLogger interface. func (e *Error) ToLog() (interface{}, error) { b, err := json.Marshal(e) if err != nil { return nil, WrapErrorISE(err, "error marshaling authority.Error for logging") } return string(b), nil } // WriteError writes to w a JSON representation of the given error. func WriteError(w http.ResponseWriter, err *Error) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(err.StatusCode()) err.Message = err.Err.Error() // Write errors in the response writer if rl, ok := w.(logging.ResponseLogger); ok { rl.WithFields(map[string]interface{}{ "error": err.Err, }) if os.Getenv("STEPDEBUG") == "1" { if e, ok := err.Err.(errs.StackTracer); ok { rl.WithFields(map[string]interface{}{ "stack-trace": fmt.Sprintf("%+v", e), }) } } } if err := json.NewEncoder(w).Encode(err); err != nil { log.Println(err) } }