garm/vendor/github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery/error.go
Gabriel Adrian Samfira c61b7fd268
Update go modules
Signed-off-by: Gabriel Adrian Samfira <gsamfira@cloudbasesolutions.com>
2023-03-12 16:22:37 +02:00

359 lines
11 KiB
Go

package httpbakery
import (
"context"
"encoding/json"
"fmt"
"net/http"
"strconv"
"gopkg.in/errgo.v1"
"gopkg.in/httprequest.v1"
"github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery"
"github.com/go-macaroon-bakery/macaroon-bakery/v3/internal/httputil"
)
// ErrorCode holds an error code that classifies
// an error returned from a bakery HTTP handler.
type ErrorCode string
func (e ErrorCode) Error() string {
return string(e)
}
func (e ErrorCode) ErrorCode() ErrorCode {
return e
}
const (
ErrBadRequest = ErrorCode("bad request")
ErrDischargeRequired = ErrorCode("macaroon discharge required")
ErrInteractionRequired = ErrorCode("interaction required")
ErrInteractionMethodNotFound = ErrorCode("discharger does not provide an supported interaction method")
ErrPermissionDenied = ErrorCode("permission denied")
)
var httpReqServer = httprequest.Server{
ErrorMapper: ErrorToResponse,
}
// WriteError writes the given bakery error to w.
func WriteError(ctx context.Context, w http.ResponseWriter, err error) {
httpReqServer.WriteError(ctx, w, err)
}
// Error holds the type of a response from an httpbakery HTTP request,
// marshaled as JSON.
//
// Note: Do not construct Error values with ErrDischargeRequired or
// ErrInteractionRequired codes directly - use the
// NewDischargeRequiredError or NewInteractionRequiredError
// functions instead.
type Error struct {
Code ErrorCode `json:",omitempty"`
Message string `json:",omitempty"`
Info *ErrorInfo `json:",omitempty"`
// version holds the protocol version that was used
// to create the error (see NewDischargeRequiredError).
version bakery.Version
}
// ErrorInfo holds additional information provided
// by an error.
type ErrorInfo struct {
// Macaroon may hold a macaroon that, when
// discharged, may allow access to a service.
// This field is associated with the ErrDischargeRequired
// error code.
Macaroon *bakery.Macaroon `json:",omitempty"`
// MacaroonPath holds the URL path to be associated
// with the macaroon. The macaroon is potentially
// valid for all URLs under the given path.
// If it is empty, the macaroon will be associated with
// the original URL from which the error was returned.
MacaroonPath string `json:",omitempty"`
// CookieNameSuffix holds the desired cookie name suffix to be
// associated with the macaroon. The actual name used will be
// ("macaroon-" + CookieName). Clients may ignore this field -
// older clients will always use ("macaroon-" +
// macaroon.Signature() in hex).
CookieNameSuffix string `json:",omitempty"`
// The following fields are associated with the
// ErrInteractionRequired error code.
// InteractionMethods holds the set of methods that the
// third party supports for completing the discharge.
// See InteractionMethod for a more convenient
// accessor method.
InteractionMethods map[string]*json.RawMessage `json:",omitempty"`
// LegacyVisitURL holds a URL that the client should visit
// in a web browser to authenticate themselves.
// This is deprecated - it is superceded by the InteractionMethods
// field.
LegacyVisitURL string `json:"VisitURL,omitempty"`
// LegacyWaitURL holds a URL that the client should visit
// to acquire the discharge macaroon. A GET on
// this URL will block until the client has authenticated,
// and then it will return the discharge macaroon.
// This is deprecated - it is superceded by the InteractionMethods
// field.
LegacyWaitURL string `json:"WaitURL,omitempty"`
}
// SetInteraction sets the information for a particular
// interaction kind to v. The error should be an interaction-required
// error. This method will panic if v cannot be JSON-marshaled.
// It is expected that interaction implementations will
// implement type-safe wrappers for this method,
// so you should not need to call it directly.
func (e *Error) SetInteraction(kind string, v interface{}) {
if e.Info == nil {
e.Info = new(ErrorInfo)
}
if e.Info.InteractionMethods == nil {
e.Info.InteractionMethods = make(map[string]*json.RawMessage)
}
data, err := json.Marshal(v)
if err != nil {
panic(err)
}
m := json.RawMessage(data)
e.Info.InteractionMethods[kind] = &m
}
// InteractionMethod checks whether the error is an InteractionRequired error
// that implements the method with the given name, and JSON-unmarshals the
// method-specific data into x.
func (e *Error) InteractionMethod(kind string, x interface{}) error {
if e.Info == nil || e.Code != ErrInteractionRequired {
return errgo.Newf("not an interaction-required error (code %v)", e.Code)
}
entry := e.Info.InteractionMethods[kind]
if entry == nil {
return errgo.WithCausef(nil, ErrInteractionMethodNotFound, "interaction method %q not found", kind)
}
if err := json.Unmarshal(*entry, x); err != nil {
return errgo.Notef(err, "cannot unmarshal data for interaction method %q", kind)
}
return nil
}
func (e *Error) Error() string {
return e.Message
}
func (e *Error) ErrorCode() ErrorCode {
return e.Code
}
// ErrorInfo returns additional information
// about the error.
// TODO return interface{} here?
func (e *Error) ErrorInfo() *ErrorInfo {
return e.Info
}
// ErrorToResponse returns the HTTP status and an error body to be
// marshaled as JSON for the given error. This allows a third party
// package to integrate bakery errors into their error responses when
// they encounter an error with a *bakery.Error cause.
func ErrorToResponse(ctx context.Context, err error) (int, interface{}) {
errorBody := errorResponseBody(err)
var body interface{} = errorBody
status := http.StatusInternalServerError
switch errorBody.Code {
case ErrBadRequest:
status = http.StatusBadRequest
case ErrPermissionDenied:
status = http.StatusUnauthorized
case ErrDischargeRequired, ErrInteractionRequired:
switch errorBody.version {
case bakery.Version0:
status = http.StatusProxyAuthRequired
case bakery.Version1, bakery.Version2, bakery.Version3:
status = http.StatusUnauthorized
body = httprequest.CustomHeader{
Body: body,
SetHeaderFunc: setAuthenticateHeader,
}
default:
panic(fmt.Sprintf("out of range version number %v", errorBody.version))
}
}
return status, body
}
func setAuthenticateHeader(h http.Header) {
h.Set("WWW-Authenticate", "Macaroon")
}
type errorInfoer interface {
ErrorInfo() *ErrorInfo
}
type errorCoder interface {
ErrorCode() ErrorCode
}
// errorResponse returns an appropriate error
// response for the provided error.
func errorResponseBody(err error) *Error {
var errResp Error
cause := errgo.Cause(err)
if cause, ok := cause.(*Error); ok {
// It's an Error already. Preserve the wrapped
// error message but copy everything else.
errResp = *cause
errResp.Message = err.Error()
return &errResp
}
// It's not an error. Preserve as much info as
// we can find.
errResp.Message = err.Error()
if coder, ok := cause.(errorCoder); ok {
errResp.Code = coder.ErrorCode()
}
if infoer, ok := cause.(errorInfoer); ok {
errResp.Info = infoer.ErrorInfo()
}
return &errResp
}
// NewInteractionRequiredError returns an error of type *Error
// that requests an interaction from the client in response
// to the given request. The originalErr value describes the original
// error - if it is nil, a default message will be provided.
//
// This function should be used in preference to creating the Error value
// directly, as it sets the bakery protocol version correctly in the error.
//
// The returned error does not support any interaction kinds.
// Use kind-specific SetInteraction methods (for example
// WebBrowserInteractor.SetInteraction) to add supported
// interaction kinds.
//
// Note that WebBrowserInteractor.SetInteraction should always be called
// for legacy clients to maintain backwards compatibility.
func NewInteractionRequiredError(originalErr error, req *http.Request) *Error {
if originalErr == nil {
originalErr = ErrInteractionRequired
}
return &Error{
Message: originalErr.Error(),
version: RequestVersion(req),
Code: ErrInteractionRequired,
}
}
type DischargeRequiredErrorParams struct {
// Macaroon holds the macaroon that needs to be discharged
// by the client.
Macaroon *bakery.Macaroon
// OriginalError holds the reason that the discharge-required
// error was created. If it's nil, ErrDischargeRequired will
// be used.
OriginalError error
// CookiePath holds the path for the client to give the cookie
// holding the discharged macaroon. If it's empty, then a
// relative path from the request URL path to / will be used if
// Request is provided, or "/" otherwise.
CookiePath string
// CookieNameSuffix holds the suffix for the client
// to give the cookie holding the discharged macaroon
// (after the "macaroon-" prefix).
// If it's empty, "auth" will be used.
CookieNameSuffix string
// Request holds the request that the error is in response to.
// It is used to form the cookie path if CookiePath is empty.
Request *http.Request
}
// NewDischargeRequiredErrorWithVersion returns an error of type *Error
// that contains a macaroon to the client and acts as a request that the
// macaroon be discharged to authorize the request.
//
// The client is responsible for discharging the macaroon and
// storing it as a cookie (or including it as a Macaroon header)
// to be used for the subsequent request.
func NewDischargeRequiredError(p DischargeRequiredErrorParams) error {
if p.OriginalError == nil {
p.OriginalError = ErrDischargeRequired
}
if p.CookiePath == "" {
p.CookiePath = "/"
if p.Request != nil {
path, err := httputil.RelativeURLPath(p.Request.URL.Path, "/")
if err == nil {
p.CookiePath = path
}
}
}
if p.CookieNameSuffix == "" {
p.CookieNameSuffix = "auth"
}
return &Error{
version: p.Macaroon.Version(),
Message: p.OriginalError.Error(),
Code: ErrDischargeRequired,
Info: &ErrorInfo{
Macaroon: p.Macaroon,
MacaroonPath: p.CookiePath,
CookieNameSuffix: p.CookieNameSuffix,
},
}
}
// BakeryProtocolHeader is the header that HTTP clients should set
// to determine the bakery protocol version. If it is 0 or missing,
// a discharge-required error response will be returned with HTTP status 407;
// if it is 1, the response will have status 401 with the WWW-Authenticate
// header set to "Macaroon".
const BakeryProtocolHeader = "Bakery-Protocol-Version"
// RequestVersion determines the bakery protocol version from a client
// request. If the protocol cannot be determined, or is invalid, the
// original version of the protocol is used. If a later version is
// found, the latest known version is used, which is OK because versions
// are backwardly compatible.
//
// TODO as there are no known version 0 clients, default to version 1
// instead.
func RequestVersion(req *http.Request) bakery.Version {
vs := req.Header.Get(BakeryProtocolHeader)
if vs == "" {
// No header - use backward compatibility mode.
return bakery.Version0
}
x, err := strconv.Atoi(vs)
if err != nil || x < 0 {
// Badly formed header - use backward compatibility mode.
return bakery.Version0
}
v := bakery.Version(x)
if v > bakery.LatestVersion {
// Later version than we know about - use the
// latest version that we can.
return bakery.LatestVersion
}
return v
}
func isDischargeRequiredError(err error) bool {
respErr, ok := errgo.Cause(err).(*Error)
if !ok {
return false
}
return respErr.Code == ErrDischargeRequired
}