367 lines
12 KiB
Go
367 lines
12 KiB
Go
package httpbakery
|
|
|
|
import (
|
|
"context"
|
|
"encoding/base64"
|
|
"net/http"
|
|
"path"
|
|
"unicode/utf8"
|
|
|
|
"github.com/julienschmidt/httprouter"
|
|
"gopkg.in/errgo.v1"
|
|
"gopkg.in/httprequest.v1"
|
|
"gopkg.in/macaroon.v2"
|
|
|
|
"github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery"
|
|
"github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/checkers"
|
|
)
|
|
|
|
// ThirdPartyCaveatChecker is used to check third party caveats.
|
|
// This interface is deprecated and included only for backward
|
|
// compatibility; ThirdPartyCaveatCheckerP should be used instead.
|
|
type ThirdPartyCaveatChecker interface {
|
|
// CheckThirdPartyCaveat is like ThirdPartyCaveatCheckerP.CheckThirdPartyCaveat
|
|
// except that it uses separate arguments instead of a struct arg.
|
|
CheckThirdPartyCaveat(ctx context.Context, info *bakery.ThirdPartyCaveatInfo, req *http.Request, token *DischargeToken) ([]checkers.Caveat, error)
|
|
}
|
|
|
|
// ThirdPartyCaveatCheckerP is used to check third party caveats.
|
|
// The "P" stands for "Params" - this was added after ThirdPartyCaveatChecker
|
|
// which can't be removed without breaking backwards compatibility.
|
|
type ThirdPartyCaveatCheckerP interface {
|
|
// CheckThirdPartyCaveat is used to check whether a client
|
|
// making the given request should be allowed a discharge for
|
|
// the p.Info.Condition. On success, the caveat will be discharged,
|
|
// with any returned caveats also added to the discharge
|
|
// macaroon.
|
|
//
|
|
// The p.Token field, if non-nil, is a token obtained from
|
|
// Interactor.Interact as the result of a discharge interaction
|
|
// after an interaction required error.
|
|
//
|
|
// Note than when used in the context of a discharge handler
|
|
// created by Discharger, any returned errors will be marshaled
|
|
// as documented in DischargeHandler.ErrorMapper.
|
|
CheckThirdPartyCaveat(ctx context.Context, p ThirdPartyCaveatCheckerParams) ([]checkers.Caveat, error)
|
|
}
|
|
|
|
// ThirdPartyCaveatCheckerParams holds the parameters passed to
|
|
// CheckThirdPartyCaveatP.
|
|
type ThirdPartyCaveatCheckerParams struct {
|
|
// Caveat holds information about the caveat being discharged.
|
|
Caveat *bakery.ThirdPartyCaveatInfo
|
|
|
|
// Token holds the discharge token provided by the client, if any.
|
|
Token *DischargeToken
|
|
|
|
// Req holds the HTTP discharge request.
|
|
Request *http.Request
|
|
|
|
// Response holds the HTTP response writer. Implementations
|
|
// must not call its WriteHeader or Write methods.
|
|
Response http.ResponseWriter
|
|
}
|
|
|
|
// ThirdPartyCaveatCheckerFunc implements ThirdPartyCaveatChecker
|
|
// by calling a function.
|
|
type ThirdPartyCaveatCheckerFunc func(ctx context.Context, req *http.Request, info *bakery.ThirdPartyCaveatInfo, token *DischargeToken) ([]checkers.Caveat, error)
|
|
|
|
func (f ThirdPartyCaveatCheckerFunc) CheckThirdPartyCaveat(ctx context.Context, info *bakery.ThirdPartyCaveatInfo, req *http.Request, token *DischargeToken) ([]checkers.Caveat, error) {
|
|
return f(ctx, req, info, token)
|
|
}
|
|
|
|
// ThirdPartyCaveatCheckerPFunc implements ThirdPartyCaveatCheckerP
|
|
// by calling a function.
|
|
type ThirdPartyCaveatCheckerPFunc func(ctx context.Context, p ThirdPartyCaveatCheckerParams) ([]checkers.Caveat, error)
|
|
|
|
func (f ThirdPartyCaveatCheckerPFunc) CheckThirdPartyCaveat(ctx context.Context, p ThirdPartyCaveatCheckerParams) ([]checkers.Caveat, error) {
|
|
return f(ctx, p)
|
|
}
|
|
|
|
// newDischargeClient returns a discharge client that addresses the
|
|
// third party discharger at the given location URL and uses
|
|
// the given client to make HTTP requests.
|
|
//
|
|
// If client is nil, http.DefaultClient is used.
|
|
func newDischargeClient(location string, client httprequest.Doer) *dischargeClient {
|
|
if client == nil {
|
|
client = http.DefaultClient
|
|
}
|
|
return &dischargeClient{
|
|
Client: httprequest.Client{
|
|
BaseURL: location,
|
|
Doer: client,
|
|
UnmarshalError: unmarshalError,
|
|
},
|
|
}
|
|
}
|
|
|
|
// Discharger holds parameters for creating a new Discharger.
|
|
type DischargerParams struct {
|
|
// CheckerP is used to actually check the caveats.
|
|
// This will be used in preference to Checker.
|
|
CheckerP ThirdPartyCaveatCheckerP
|
|
|
|
// Checker is used to actually check the caveats.
|
|
// This should be considered deprecated and will be ignored if CheckerP is set.
|
|
Checker ThirdPartyCaveatChecker
|
|
|
|
// Key holds the key pair of the discharger.
|
|
Key *bakery.KeyPair
|
|
|
|
// Locator is used to find public keys when adding
|
|
// third-party caveats on discharge macaroons.
|
|
// If this is nil, no third party caveats may be added.
|
|
Locator bakery.ThirdPartyLocator
|
|
|
|
// ErrorToResponse is used to convert errors returned by the third
|
|
// party caveat checker to the form that will be JSON-marshaled
|
|
// on the wire. If zero, this defaults to ErrorToResponse.
|
|
// If set, it should handle errors that it does not understand
|
|
// by falling back to calling ErrorToResponse to ensure
|
|
// that the standard bakery errors are marshaled in the expected way.
|
|
ErrorToResponse func(ctx context.Context, err error) (int, interface{})
|
|
}
|
|
|
|
// Discharger represents a third-party caveat discharger.
|
|
// can discharge caveats in an HTTP server.
|
|
//
|
|
// The name space served by dischargers is as follows.
|
|
// All parameters can be provided either as URL attributes
|
|
// or form attributes. The result is always formatted as a JSON
|
|
// object.
|
|
//
|
|
// On failure, all endpoints return an error described by
|
|
// the Error type.
|
|
//
|
|
// POST /discharge
|
|
// params:
|
|
// id: all-UTF-8 third party caveat id
|
|
// id64: non-padded URL-base64 encoded caveat id
|
|
// macaroon-id: (optional) id to give to discharge macaroon (defaults to id)
|
|
// token: (optional) value of discharge token
|
|
// token64: (optional) base64-encoded value of discharge token.
|
|
// token-kind: (mandatory if token or token64 provided) discharge token kind.
|
|
// result on success (http.StatusOK):
|
|
// {
|
|
// Macaroon *macaroon.Macaroon
|
|
// }
|
|
//
|
|
// GET /publickey
|
|
// result:
|
|
// public key of service
|
|
// expiry time of key
|
|
type Discharger struct {
|
|
p DischargerParams
|
|
}
|
|
|
|
// NewDischarger returns a new third-party caveat discharger
|
|
// using the given parameters.
|
|
func NewDischarger(p DischargerParams) *Discharger {
|
|
if p.ErrorToResponse == nil {
|
|
p.ErrorToResponse = ErrorToResponse
|
|
}
|
|
if p.Locator == nil {
|
|
p.Locator = emptyLocator{}
|
|
}
|
|
if p.CheckerP == nil {
|
|
p.CheckerP = ThirdPartyCaveatCheckerPFunc(func(ctx context.Context, cp ThirdPartyCaveatCheckerParams) ([]checkers.Caveat, error) {
|
|
return p.Checker.CheckThirdPartyCaveat(ctx, cp.Caveat, cp.Request, cp.Token)
|
|
})
|
|
}
|
|
return &Discharger{
|
|
p: p,
|
|
}
|
|
}
|
|
|
|
type emptyLocator struct{}
|
|
|
|
func (emptyLocator) ThirdPartyInfo(ctx context.Context, loc string) (bakery.ThirdPartyInfo, error) {
|
|
return bakery.ThirdPartyInfo{}, bakery.ErrNotFound
|
|
}
|
|
|
|
// AddMuxHandlers adds handlers to the given ServeMux to provide
|
|
// a third-party caveat discharge service.
|
|
func (d *Discharger) AddMuxHandlers(mux *http.ServeMux, rootPath string) {
|
|
for _, h := range d.Handlers() {
|
|
// Note: this only works because we don't have any wildcard
|
|
// patterns in the discharger paths.
|
|
mux.Handle(path.Join(rootPath, h.Path), mkHTTPHandler(h.Handle))
|
|
}
|
|
}
|
|
|
|
// Handlers returns a slice of handlers that can handle a third-party
|
|
// caveat discharge service when added to an httprouter.Router.
|
|
// TODO provide some way of customizing the context so that
|
|
// ErrorToResponse can see a request-specific context.
|
|
func (d *Discharger) Handlers() []httprequest.Handler {
|
|
f := func(p httprequest.Params) (dischargeHandler, context.Context, error) {
|
|
return dischargeHandler{
|
|
discharger: d,
|
|
}, p.Context, nil
|
|
}
|
|
srv := httprequest.Server{
|
|
ErrorMapper: d.p.ErrorToResponse,
|
|
}
|
|
return srv.Handlers(f)
|
|
}
|
|
|
|
//go:generate httprequest-generate-client github.com/go-macaroon-bakery/macaroon-bakery/v3-unstable/httpbakery dischargeHandler dischargeClient
|
|
|
|
// dischargeHandler is the type used to define the httprequest handler
|
|
// methods for a discharger.
|
|
type dischargeHandler struct {
|
|
discharger *Discharger
|
|
}
|
|
|
|
// dischargeRequest is a request to create a macaroon that discharges the
|
|
// supplied third-party caveat. Discharging caveats will normally be
|
|
// handled by the bakery it would be unusual to use this type directly in
|
|
// client software.
|
|
type dischargeRequest struct {
|
|
httprequest.Route `httprequest:"POST /discharge"`
|
|
Id string `httprequest:"id,form,omitempty"`
|
|
Id64 string `httprequest:"id64,form,omitempty"`
|
|
Caveat string `httprequest:"caveat64,form,omitempty"`
|
|
Token string `httprequest:"token,form,omitempty"`
|
|
Token64 string `httprequest:"token64,form,omitempty"`
|
|
TokenKind string `httprequest:"token-kind,form,omitempty"`
|
|
}
|
|
|
|
// dischargeResponse contains the response from a /discharge POST request.
|
|
type dischargeResponse struct {
|
|
Macaroon *bakery.Macaroon `json:",omitempty"`
|
|
}
|
|
|
|
// Discharge discharges a third party caveat.
|
|
func (h dischargeHandler) Discharge(p httprequest.Params, r *dischargeRequest) (*dischargeResponse, error) {
|
|
id, err := maybeBase64Decode(r.Id, r.Id64)
|
|
if err != nil {
|
|
return nil, errgo.Notef(err, "bad caveat id")
|
|
}
|
|
var caveat []byte
|
|
if r.Caveat != "" {
|
|
// Note that it's important that when r.Caveat is empty,
|
|
// we leave DischargeParams.Caveat as nil (Base64Decode
|
|
// always returns a non-nil byte slice).
|
|
caveat1, err := macaroon.Base64Decode([]byte(r.Caveat))
|
|
if err != nil {
|
|
return nil, errgo.Notef(err, "bad base64-encoded caveat: %v", err)
|
|
}
|
|
caveat = caveat1
|
|
}
|
|
tokenVal, err := maybeBase64Decode(r.Token, r.Token64)
|
|
if err != nil {
|
|
return nil, errgo.Notef(err, "bad discharge token")
|
|
}
|
|
var token *DischargeToken
|
|
if len(tokenVal) != 0 {
|
|
if r.TokenKind == "" {
|
|
return nil, errgo.Notef(err, "discharge token provided without token kind")
|
|
}
|
|
token = &DischargeToken{
|
|
Kind: r.TokenKind,
|
|
Value: tokenVal,
|
|
}
|
|
}
|
|
m, err := bakery.Discharge(p.Context, bakery.DischargeParams{
|
|
Id: id,
|
|
Caveat: caveat,
|
|
Key: h.discharger.p.Key,
|
|
Checker: bakery.ThirdPartyCaveatCheckerFunc(
|
|
func(ctx context.Context, cav *bakery.ThirdPartyCaveatInfo) ([]checkers.Caveat, error) {
|
|
return h.discharger.p.CheckerP.CheckThirdPartyCaveat(ctx, ThirdPartyCaveatCheckerParams{
|
|
Caveat: cav,
|
|
Request: p.Request,
|
|
Response: p.Response,
|
|
Token: token,
|
|
})
|
|
},
|
|
),
|
|
Locator: h.discharger.p.Locator,
|
|
})
|
|
if err != nil {
|
|
return nil, errgo.NoteMask(err, "cannot discharge", errgo.Any)
|
|
}
|
|
return &dischargeResponse{m}, nil
|
|
}
|
|
|
|
// publicKeyRequest specifies the /publickey endpoint.
|
|
type publicKeyRequest struct {
|
|
httprequest.Route `httprequest:"GET /publickey"`
|
|
}
|
|
|
|
// publicKeyResponse is the response to a /publickey GET request.
|
|
type publicKeyResponse struct {
|
|
PublicKey *bakery.PublicKey
|
|
}
|
|
|
|
// dischargeInfoRequest specifies the /discharge/info endpoint.
|
|
type dischargeInfoRequest struct {
|
|
httprequest.Route `httprequest:"GET /discharge/info"`
|
|
}
|
|
|
|
// dischargeInfoResponse is the response to a /discharge/info GET
|
|
// request.
|
|
type dischargeInfoResponse struct {
|
|
PublicKey *bakery.PublicKey
|
|
Version bakery.Version
|
|
}
|
|
|
|
// PublicKey returns the public key of the discharge service.
|
|
func (h dischargeHandler) PublicKey(*publicKeyRequest) (publicKeyResponse, error) {
|
|
return publicKeyResponse{
|
|
PublicKey: &h.discharger.p.Key.Public,
|
|
}, nil
|
|
}
|
|
|
|
// DischargeInfo returns information on the discharger.
|
|
func (h dischargeHandler) DischargeInfo(*dischargeInfoRequest) (dischargeInfoResponse, error) {
|
|
return dischargeInfoResponse{
|
|
PublicKey: &h.discharger.p.Key.Public,
|
|
Version: bakery.LatestVersion,
|
|
}, nil
|
|
}
|
|
|
|
// mkHTTPHandler converts an httprouter handler to an http.Handler,
|
|
// assuming that the httprouter handler has no wildcard path
|
|
// parameters.
|
|
func mkHTTPHandler(h httprouter.Handle) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
|
h(w, req, nil)
|
|
})
|
|
}
|
|
|
|
// maybeBase64Encode encodes b as is if it's
|
|
// OK to be passed as a URL form parameter,
|
|
// or encoded as base64 otherwise.
|
|
func maybeBase64Encode(b []byte) (s, s64 string) {
|
|
if utf8.Valid(b) {
|
|
valid := true
|
|
for _, c := range b {
|
|
if c < 32 || c == 127 {
|
|
valid = false
|
|
break
|
|
}
|
|
}
|
|
if valid {
|
|
return string(b), ""
|
|
}
|
|
}
|
|
return "", base64.RawURLEncoding.EncodeToString(b)
|
|
}
|
|
|
|
// maybeBase64Decode implements the inverse of maybeBase64Encode.
|
|
func maybeBase64Decode(s, s64 string) ([]byte, error) {
|
|
if s64 != "" {
|
|
data, err := macaroon.Base64Decode([]byte(s64))
|
|
if err != nil {
|
|
return nil, errgo.Mask(err)
|
|
}
|
|
if len(data) == 0 {
|
|
return nil, nil
|
|
}
|
|
return data, nil
|
|
}
|
|
return []byte(s), nil
|
|
}
|