pax_global_header00006660000000000000000000000064146442741550014526gustar00rootroot0000000000000052 comment=7e8973aec59c9b11e62dd3281069ef71f158cc0a macaroon-bakery-3.0.2/000077500000000000000000000000001464427415500146025ustar00rootroot00000000000000macaroon-bakery-3.0.2/.github/000077500000000000000000000000001464427415500161425ustar00rootroot00000000000000macaroon-bakery-3.0.2/.github/dependabot.yaml000066400000000000000000000004771464427415500211430ustar00rootroot00000000000000version: 2 updates: - package-ecosystem: "github-actions" directory: "/" schedule: # Check for updates to GitHub Actions every weekday interval: "daily" - package-ecosystem: "gomod" directory: "/" schedule: # Check for updates to go modules every weekday interval: "daily" macaroon-bakery-3.0.2/.github/workflows/000077500000000000000000000000001464427415500201775ustar00rootroot00000000000000macaroon-bakery-3.0.2/.github/workflows/ci.yaml000066400000000000000000000023431464427415500214600ustar00rootroot00000000000000name: CI on: [push, pull_request] jobs: build_test: name: Build and Test strategy: matrix: go: ['1.17','1.18','1.19'] runs-on: ubuntu-latest container: image: ubuntu volumes: - /etc/ssl/certs:/etc/ssl/certs services: postgres: image: ubuntu/postgres env: POSTGRES_PASSWORD: password # Set health checks to wait until postgres has started options: >- --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 mongo: image: mongo:4.4-bionic steps: - uses: actions/checkout@v2.3.4 - uses: actions/setup-go@v2.1.3 with: go-version: ${{ matrix.go }} stable: false - uses: actions/cache@v2.1.4 with: path: ~/go/pkg/mod key: ubuntu-go-${{ hashFiles('**/go.sum') }} restore-keys: | ubuntu-go- - name: Install dependencies run: apt-get update -y && apt-get install -y gcc git-core - name: Build and Test run: go test ./... env: MGOCONNECTIONSTRING: mongo PGHOST: postgres PGPASSWORD: password PGSSLMODE: disable PGUSER: postgres macaroon-bakery-3.0.2/.gitignore000066400000000000000000000000161464427415500165670ustar00rootroot00000000000000*.test /.idea/macaroon-bakery-3.0.2/LICENSE000066400000000000000000000211331464427415500156070ustar00rootroot00000000000000Copyright © 2014, Roger Peppe, Canonical Inc. This software is licensed under the LGPLv3, included below. As a special exception to the GNU Lesser General Public License version 3 ("LGPL3"), the copyright holders of this Library give you permission to convey to a third party a Combined Work that links statically or dynamically to this Library without providing any Minimal Corresponding Source or Minimal Application Code as set out in 4d or providing the installation information set out in section 4e, provided that you comply with the other provisions of LGPL3 and provided that you meet, for the Application the terms and conditions of the license(s) which apply to the Application. Except as stated in this special exception, the provisions of LGPL3 will continue to comply in full to this Library. If you modify this Library, you may apply this exception to your version of this Library, but you are not obliged to do so. If you do not wish to do so, delete this exception statement from your version. This exception does not (and cannot) modify any license terms which apply to the Application, with which you must still comply. GNU LESSER GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. This version of the GNU Lesser General Public License incorporates the terms and conditions of version 3 of the GNU General Public License, supplemented by the additional permissions listed below. 0. Additional Definitions. As used herein, "this License" refers to version 3 of the GNU Lesser General Public License, and the "GNU GPL" refers to version 3 of the GNU General Public License. "The Library" refers to a covered work governed by this License, other than an Application or a Combined Work as defined below. An "Application" is any work that makes use of an interface provided by the Library, but which is not otherwise based on the Library. Defining a subclass of a class defined by the Library is deemed a mode of using an interface provided by the Library. A "Combined Work" is a work produced by combining or linking an Application with the Library. The particular version of the Library with which the Combined Work was made is also called the "Linked Version". The "Minimal Corresponding Source" for a Combined Work means the Corresponding Source for the Combined Work, excluding any source code for portions of the Combined Work that, considered in isolation, are based on the Application, and not on the Linked Version. The "Corresponding Application Code" for a Combined Work means the object code and/or source code for the Application, including any data and utility programs needed for reproducing the Combined Work from the Application, but excluding the System Libraries of the Combined Work. 1. Exception to Section 3 of the GNU GPL. You may convey a covered work under sections 3 and 4 of this License without being bound by section 3 of the GNU GPL. 2. Conveying Modified Versions. If you modify a copy of the Library, and, in your modifications, a facility refers to a function or data to be supplied by an Application that uses the facility (other than as an argument passed when the facility is invoked), then you may convey a copy of the modified version: a) under this License, provided that you make a good faith effort to ensure that, in the event an Application does not supply the function or data, the facility still operates, and performs whatever part of its purpose remains meaningful, or b) under the GNU GPL, with none of the additional permissions of this License applicable to that copy. 3. Object Code Incorporating Material from Library Header Files. The object code form of an Application may incorporate material from a header file that is part of the Library. You may convey such object code under terms of your choice, provided that, if the incorporated material is not limited to numerical parameters, data structure layouts and accessors, or small macros, inline functions and templates (ten or fewer lines in length), you do both of the following: a) Give prominent notice with each copy of the object code that the Library is used in it and that the Library and its use are covered by this License. b) Accompany the object code with a copy of the GNU GPL and this license document. 4. Combined Works. You may convey a Combined Work under terms of your choice that, taken together, effectively do not restrict modification of the portions of the Library contained in the Combined Work and reverse engineering for debugging such modifications, if you also do each of the following: a) Give prominent notice with each copy of the Combined Work that the Library is used in it and that the Library and its use are covered by this License. b) Accompany the Combined Work with a copy of the GNU GPL and this license document. c) For a Combined Work that displays copyright notices during execution, include the copyright notice for the Library among these notices, as well as a reference directing the user to the copies of the GNU GPL and this license document. d) Do one of the following: 0) Convey the Minimal Corresponding Source under the terms of this License, and the Corresponding Application Code in a form suitable for, and under terms that permit, the user to recombine or relink the Application with a modified version of the Linked Version to produce a modified Combined Work, in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source. 1) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (a) uses at run time a copy of the Library already present on the user's computer system, and (b) will operate properly with a modified version of the Library that is interface-compatible with the Linked Version. e) Provide Installation Information, but only if you would otherwise be required to provide such information under section 6 of the GNU GPL, and only to the extent that such information is necessary to install and execute a modified version of the Combined Work produced by recombining or relinking the Application with a modified version of the Linked Version. (If you use option 4d0, the Installation Information must accompany the Minimal Corresponding Source and Corresponding Application Code. If you use option 4d1, you must provide the Installation Information in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source.) 5. Combined Libraries. You may place library facilities that are a work based on the Library side by side in a single library together with other library facilities that are not Applications and are not covered by this License, and convey such a combined library under terms of your choice, if you do both of the following: a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities, conveyed under the terms of this License. b) Give prominent notice with the combined library that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. 6. Revised Versions of the GNU Lesser General Public License. The Free Software Foundation may publish revised and/or new versions of the GNU Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Library as you received it specifies that a certain numbered version of the GNU Lesser General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that published version or of any later version published by the Free Software Foundation. If the Library as you received it does not specify a version number of the GNU Lesser General Public License, you may choose any version of the GNU Lesser General Public License ever published by the Free Software Foundation. If the Library as you received it specifies that a proxy can decide whether future versions of the GNU Lesser General Public License shall apply, that proxy's public statement of acceptance of any version is permanent authorization for you to choose that version for the Library. macaroon-bakery-3.0.2/README.md000066400000000000000000000006471464427415500160700ustar00rootroot00000000000000# The macaroon bakery This repository is a companion to http://github.com/go-macaroon . It holds higher level operations for building systems with macaroons. For documentation, see: - http://godoc.org/github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery - http://godoc.org/github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery - http://godoc.org/github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/checkers macaroon-bakery-3.0.2/TODO000066400000000000000000000002241464427415500152700ustar00rootroot00000000000000all: - when API is stable, move to gopkg.in/macaroon.v1 macaroon: - change all signature calculations to correspond exactly with libmacaroons. macaroon-bakery-3.0.2/bakery/000077500000000000000000000000001464427415500160575ustar00rootroot00000000000000macaroon-bakery-3.0.2/bakery/authstore_test.go000066400000000000000000000051061464427415500214650ustar00rootroot00000000000000package bakery_test import ( "context" "encoding/json" "gopkg.in/errgo.v1" "gopkg.in/macaroon.v2" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/checkers" ) type macaroonStore struct { rootKeyStore bakery.RootKeyStore key *bakery.KeyPair locator bakery.ThirdPartyLocator } // newMacaroonStore returns a MacaroonVerifier implementation // that stores root keys in memory and puts all operations // in the macaroon id. func newMacaroonStore(locator bakery.ThirdPartyLocator) *macaroonStore { return &macaroonStore{ rootKeyStore: bakery.NewMemRootKeyStore(), key: mustGenerateKey(), locator: locator, } } type macaroonId struct { Id []byte Ops []bakery.Op } func (s *macaroonStore) NewMacaroon(ctx context.Context, ops []bakery.Op, caveats []checkers.Caveat, ns *checkers.Namespace) (*bakery.Macaroon, error) { rootKey, id, err := s.rootKeyStore.RootKey(ctx) if err != nil { return nil, errgo.Mask(err) } mid := macaroonId{ Id: id, Ops: ops, } data, _ := json.Marshal(mid) m, err := bakery.NewMacaroon(rootKey, data, "", bakery.LatestVersion, ns) if err != nil { return nil, errgo.Mask(err) } if err := m.AddCaveats(ctx, caveats, s.key, s.locator); err != nil { return nil, errgo.Mask(err) } return m, nil } func (s *macaroonStore) VerifyMacaroon(ctx context.Context, ms macaroon.Slice) (ops []bakery.Op, conditions []string, err error) { if len(ms) == 0 { return nil, nil, &bakery.VerificationError{ Reason: errgo.Newf("no macaroons in slice"), } } id := ms[0].Id() var mid macaroonId if err := json.Unmarshal(id, &mid); err != nil { return nil, nil, &bakery.VerificationError{ Reason: errgo.Notef(err, "bad macaroon id"), } } rootKey, err := s.rootKeyStore.Get(ctx, mid.Id) if err != nil { if errgo.Cause(err) == bakery.ErrNotFound { return nil, nil, &bakery.VerificationError{ Reason: errgo.Notef(err, "cannot find root key"), } } return nil, nil, errgo.Notef(err, "cannot find root key") } conditions, err = ms[0].VerifySignature(rootKey, ms[1:]) if err != nil { return nil, nil, &bakery.VerificationError{ Reason: errgo.Mask(err), } } return mid.Ops, conditions, nil } // macaroonVerifierWithError is an implementation of MacaroonVerifier that // returns the given error on all store operations. type macaroonVerifierWithError struct { err error } func (s macaroonVerifierWithError) VerifyMacaroon(ctx context.Context, ms macaroon.Slice) (ops []bakery.Op, conditions []string, err error) { return nil, nil, errgo.Mask(s.err, errgo.Any) } macaroon-bakery-3.0.2/bakery/bakery.go000066400000000000000000000055531464427415500176730ustar00rootroot00000000000000package bakery import ( "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/checkers" ) // Bakery is a convenience type that contains both an Oven // and a Checker. type Bakery struct { Oven *Oven Checker *Checker } // BakeryParams holds a selection of parameters for the Oven // and the Checker created by New. // // For more fine-grained control of parameters, create the // Oven or Checker directly. // // The zero value is OK to use, but won't allow any authentication // or third party caveats to be added. type BakeryParams struct { // Logger is used to send log messages. If it is nil, // nothing will be logged. Logger Logger // Checker holds the checker used to check first party caveats. // If this is nil, New will use checkers.New(nil). Checker FirstPartyCaveatChecker // RootKeyStore holds the root key store to use. If you need to // use a different root key store for different operations, // you'll need to pass a RootKeyStoreForOps value to NewOven // directly. // // If this is nil, New will use NewMemRootKeyStore(). // Note that that is almost certain insufficient for production services // that are spread across multiple instances or that need // to persist keys across restarts. RootKeyStore RootKeyStore // Locator is used to find out information on third parties when // adding third party caveats. If this is nil, no non-local third // party caveats can be added. Locator ThirdPartyLocator // Key holds the private key of the oven. If this is nil, // no third party caveats may be added. Key *KeyPair // OpsAuthorizer is used to check whether operations are authorized // by some other already-authorized operation. If it is nil, // NewChecker will assume no operation is authorized by any // operation except itself. OpsAuthorizer OpsAuthorizer // Location holds the location to use when creating new macaroons. Location string // LegacyMacaroonOp holds the operation to associate with old // macaroons that don't have associated operations. // If this is empty, legacy macaroons will not be associated // with any operations. LegacyMacaroonOp Op } // New returns a new Bakery instance which combines an Oven with a // Checker for the convenience of callers that wish to use both // together. func New(p BakeryParams) *Bakery { if p.Checker == nil { p.Checker = checkers.New(nil) } ovenParams := OvenParams{ Key: p.Key, Namespace: p.Checker.Namespace(), Location: p.Location, Locator: p.Locator, LegacyMacaroonOp: p.LegacyMacaroonOp, } if p.RootKeyStore != nil { ovenParams.RootKeyStoreForOps = func(ops []Op) RootKeyStore { return p.RootKeyStore } } oven := NewOven(ovenParams) checker := NewChecker(CheckerParams{ Checker: p.Checker, MacaroonVerifier: oven, OpsAuthorizer: p.OpsAuthorizer, }) return &Bakery{ Oven: oven, Checker: checker, } } macaroon-bakery-3.0.2/bakery/checker.go000066400000000000000000000351761464427415500200260ustar00rootroot00000000000000package bakery import ( "context" "sort" "sync" "time" "gopkg.in/errgo.v1" "gopkg.in/macaroon.v2" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/checkers" ) // Op holds an entity and action to be authorized on that entity. type Op struct { // Entity holds the name of the entity to be authorized. // Entity names should not contain spaces and should // not start with the prefix "login" or "multi-" (conventionally, // entity names will be prefixed with the entity type followed // by a hyphen. Entity string // Action holds the action to perform on the entity, such as "read" // or "delete". It is up to the service using a checker to define // a set of operations and keep them consistent over time. Action string } // NoOp holds the empty operation, signifying no authorized // operation. This is always considered to be authorized. // See OpsAuthorizer for one place that it's used. var NoOp = Op{} // CheckerParams holds parameters for NewChecker. type CheckerParams struct { // Checker is used to check first party caveats when authorizing. // If this is nil NewChecker will use checkers.New(nil). Checker FirstPartyCaveatChecker // OpsAuthorizer is used to check whether operations are authorized // by some other already-authorized operation. If it is nil, // NewChecker will assume no operation is authorized by any // operation except itself. OpsAuthorizer OpsAuthorizer // MacaroonVerifier is used to verify macaroons. MacaroonVerifier MacaroonVerifier // Logger is used to log checker operations. If it is nil, // DefaultLogger("bakery") will be used. Logger Logger } // OpsAuthorizer is used to check whether an operation authorizes some other // operation. For example, a macaroon with an operation allowing general access to a service // might also grant access to a more specific operation. type OpsAuthorizer interface { // AuthorizeOp reports which elements of queryOps are authorized by // authorizedOp. On return, each element of the slice should represent // whether the respective element in queryOps has been authorized. // An empty returned slice indicates that no operations are authorized. // AuthorizeOps may also return third party caveats that apply to // the authorized operations. Access will only be authorized when // those caveats are discharged by the client. // // When not all operations can be authorized with the macaroons // supplied to Checker.Auth, the checker will call AuthorizeOps // with NoOp, because some operations might be authorized // regardless of authority. NoOp will always be the last // operation queried within any given Allow call. // // AuthorizeOps should only return an error if authorization cannot be checked // (for example because of a database access failure), not because // authorization was denied. AuthorizeOps(ctx context.Context, authorizedOp Op, queryOps []Op) ([]bool, []checkers.Caveat, error) } // AuthInfo information about an authorization decision. type AuthInfo struct { // Macaroons holds all the macaroons that were // passed to Auth. Macaroons []macaroon.Slice // Used records which macaroons were used in the // authorization decision. It holds one element for // each element of Macaroons. Macaroons that // were invalid or unnecessary will have a false entry. Used []bool // OpIndexes holds the index of each macaroon // that was used to authorize an operation. OpIndexes map[Op]int } // Conditions returns the first party caveat caveat conditions hat apply to // the given AuthInfo. This can be used to apply appropriate caveats // to capability macaroons granted via a Checker.Allow call. func (a *AuthInfo) Conditions() []string { var squasher caveatSquasher for i, ms := range a.Macaroons { if !a.Used[i] { continue } for _, m := range ms { for _, cav := range m.Caveats() { if len(cav.VerificationId) > 0 { continue } squasher.add(string(cav.Id)) } } } return squasher.final() } // Checker wraps a FirstPartyCaveatChecker and adds authentication and authorization checks. // // It uses macaroons as authorization tokens but it is not itself responsible for // creating the macaroons - see the Oven type (TODO) for one way of doing that. type Checker struct { FirstPartyCaveatChecker p CheckerParams } // NewChecker returns a new Checker using the given parameters. func NewChecker(p CheckerParams) *Checker { if p.Checker == nil { p.Checker = checkers.New(nil) } if p.Logger == nil { p.Logger = DefaultLogger("bakery") } return &Checker{ FirstPartyCaveatChecker: p.Checker, p: p, } } // Auth makes a new AuthChecker instance using the // given macaroons to inform authorization decisions. func (c *Checker) Auth(mss ...macaroon.Slice) *AuthChecker { return &AuthChecker{ Checker: c, macaroons: mss, } } // AuthChecker authorizes operations with respect to a user's request. type AuthChecker struct { // Checker is used to check first party caveats. *Checker macaroons []macaroon.Slice // conditions holds the first party caveat conditions // that apply to each of the above macaroons. conditions [][]string initOnce sync.Once initError error initErrors []error // authIndexes holds for each potentially authorized operation // the indexes of the macaroons that authorize it. authIndexes map[Op][]int } func (a *AuthChecker) init(ctx context.Context) error { a.initOnce.Do(func() { a.initError = a.initOnceFunc(ctx) }) return a.initError } func (a *AuthChecker) initOnceFunc(ctx context.Context) error { a.authIndexes = make(map[Op][]int) a.conditions = make([][]string, len(a.macaroons)) for i, ms := range a.macaroons { ops, conditions, err := a.p.MacaroonVerifier.VerifyMacaroon(ctx, ms) if err != nil { if !isVerificationError(err) { return errgo.Notef(err, "cannot retrieve macaroon") } a.initErrors = append(a.initErrors, errgo.Mask(err)) continue } a.p.Logger.Debugf(ctx, "macaroon %d has valid sig; ops %q, conditions %q", i, ops, conditions) // It's a valid macaroon (in principle - we haven't checked first party caveats). a.conditions[i] = conditions for _, op := range ops { a.authIndexes[op] = append(a.authIndexes[op], i) } } return nil } // Allowed returns an AuthInfo that provides information on all // operations directly authorized by the macaroons provided // to Checker.Auth. Note that this does not include operations that would be indirectly // allowed via the OpAuthorizer. // // Allowed returns an error only when there is an underlying storage failure, // not when operations are not authorized. func (a *AuthChecker) Allowed(ctx context.Context) (*AuthInfo, error) { actx, err := a.newAllowContext(ctx, nil) if err != nil { return nil, errgo.Mask(err) } for op, mindexes := range a.authIndexes { for _, mindex := range mindexes { if actx.status[mindex]&statusOK != 0 { actx.status[mindex] |= statusUsed actx.opIndexes[op] = mindex break } } } return actx.newAuthInfo(), nil } func (a *allowContext) newAuthInfo() *AuthInfo { info := &AuthInfo{ Macaroons: a.checker.macaroons, Used: make([]bool, len(a.checker.macaroons)), OpIndexes: a.opIndexes, } for i, status := range a.status { if status&statusUsed != 0 { info.Used[i] = true } } return info } // allowContext holds temporary state used by AuthChecker.allowAny. type allowContext struct { checker *AuthChecker // status holds used and authorized status of all the // request macaroons. status []macaroonStatus // opIndex holds an entry for each authorized operation // that refers to the macaroon that authorized that operation. opIndexes map[Op]int // authed holds which of the requested operations have // been authorized so far. authed []bool // need holds all of the requested operations that // are remaining to be authorized. needIndex holds the // index of each of these operations in the original operations slice need []Op needIndex []int // errors holds any errors encountered during authorization. errors []error } type macaroonStatus uint8 const ( statusOK = 1 << iota statusUsed ) func (a *AuthChecker) newAllowContext(ctx context.Context, ops []Op) (*allowContext, error) { actx := &allowContext{ checker: a, status: make([]macaroonStatus, len(a.macaroons)), authed: make([]bool, len(ops)), need: append([]Op(nil), ops...), needIndex: make([]int, len(ops)), opIndexes: make(map[Op]int), } for i := range actx.needIndex { actx.needIndex[i] = i } if err := a.init(ctx); err != nil { return actx, errgo.Mask(err) } // Check all the macaroons with respect to the current context. // Technically this is more than we need to do, because some // of the macaroons might not authorize the specific operations // we're interested in, but that's an optimisation that could happen // later if performance becomes an issue with respect to that. outer: for i, ms := range a.macaroons { ctx := checkers.ContextWithMacaroons(ctx, a.Namespace(), ms) for _, cond := range a.conditions[i] { if err := a.CheckFirstPartyCaveat(ctx, cond); err != nil { actx.addError(err) continue outer } } actx.status[i] = statusOK } return actx, nil } // Macaroons returns the macaroons that were passed // to Checker.Auth when creating the AuthChecker. func (a *AuthChecker) Macaroons() []macaroon.Slice { return a.macaroons } // Allow checks that the authorizer's request is authorized to // perform all the given operations. // // If all the operations are allowed, an AuthInfo is returned holding // details of the decision. // // If an operation was not allowed, an error will be returned which may // be *DischargeRequiredError holding the operations that remain to // be authorized in order to allow authorization to // proceed. func (a *AuthChecker) Allow(ctx context.Context, ops ...Op) (*AuthInfo, error) { actx, err := a.newAllowContext(ctx, ops) if err != nil { return nil, errgo.Mask(err) } actx.checkDirect(ctx) if len(actx.need) == 0 { return actx.newAuthInfo(), nil } caveats, err := actx.checkIndirect(ctx) if err != nil { return nil, errgo.Mask(err) } if len(actx.need) == 0 && len(caveats) == 0 { // No more ops need to be authenticated and no caveats to be discharged. return actx.newAuthInfo(), nil } a.p.Logger.Debugf(ctx, "operations still needed after auth check: %#v", actx.need) if len(caveats) == 0 || len(actx.need) > 0 { allErrors := make([]error, 0, len(a.initErrors)+len(actx.errors)) allErrors = append(allErrors, a.initErrors...) allErrors = append(allErrors, actx.errors...) var err error if len(allErrors) > 0 { // TODO return all errors? a.p.Logger.Infof(ctx, "all auth errors: %q", allErrors) err = allErrors[0] } return nil, errgo.WithCausef(err, ErrPermissionDenied, "") } return nil, &DischargeRequiredError{ Message: "some operations have extra caveats", Ops: ops, Caveats: caveats, } } // checkDirect checks which operations are directly authorized by // the macaroon operations. func (a *allowContext) checkDirect(ctx context.Context) { defer a.updateNeed() for i, op := range a.need { if op == NoOp { // NoOp is always authorized. a.authed[a.needIndex[i]] = true continue } for _, mindex := range a.checker.authIndexes[op] { if a.status[mindex]&statusOK != 0 { a.authed[a.needIndex[i]] = true a.status[mindex] |= statusUsed a.opIndexes[op] = mindex break } } } } // checkIndirect checks to see if any of the remaining operations are authorized // indirectly with the already-authorized operations. func (a *allowContext) checkIndirect(ctx context.Context) ([]checkers.Caveat, error) { if a.checker.p.OpsAuthorizer == nil { return nil, nil } var allCaveats []checkers.Caveat for op, mindexes := range a.checker.authIndexes { if len(a.need) == 0 { break } for _, mindex := range mindexes { if a.status[mindex]&statusOK == 0 { continue } ctx := checkers.ContextWithMacaroons(ctx, a.checker.Namespace(), a.checker.macaroons[mindex]) authedOK, caveats, err := a.checker.p.OpsAuthorizer.AuthorizeOps(ctx, op, a.need) if err != nil { return nil, errgo.Mask(err) } // TODO we could perhaps combine identical third party caveats here. allCaveats = append(allCaveats, caveats...) for i, ok := range authedOK { if !ok { continue } // Operation is authorized. Mark the appropriate macaroon as used, // and remove the operation from the needed list so that we don't // bother AuthorizeOps with it again. a.status[mindex] |= statusUsed a.authed[a.needIndex[i]] = true a.opIndexes[a.need[i]] = mindex } } a.updateNeed() } if len(a.need) == 0 { return allCaveats, nil } // We've still got at least one operation unauthorized. // Try to see if it can be authorized with no operation at all. authedOK, caveats, err := a.checker.p.OpsAuthorizer.AuthorizeOps(ctx, NoOp, a.need) if err != nil { return nil, errgo.Mask(err) } allCaveats = append(allCaveats, caveats...) for i, ok := range authedOK { if ok { a.authed[a.needIndex[i]] = true } } a.updateNeed() return allCaveats, nil } // updateNeed removes all authorized operations from a.need // and updates a.needIndex appropriately too. func (a *allowContext) updateNeed() { j := 0 for i, opIndex := range a.needIndex { if a.authed[opIndex] { continue } if i != j { a.need[j], a.needIndex[j] = a.need[i], a.needIndex[i] } j++ } a.need, a.needIndex = a.need[0:j], a.needIndex[0:j] } func (a *allowContext) addError(err error) { a.errors = append(a.errors, err) } // caveatSquasher rationalizes first party caveats created for a capability // by: // - including only the earliest time-before caveat. // - removing duplicates. type caveatSquasher struct { expiry time.Time conds []string } func (c *caveatSquasher) add(cond string) { if c.add0(cond) { c.conds = append(c.conds, cond) } } func (c *caveatSquasher) add0(cond string) bool { cond, args, err := checkers.ParseCaveat(cond) if err != nil { // Be safe - if we can't parse the caveat, just leave it there. return true } if cond != checkers.CondTimeBefore { return true } et, err := time.Parse(time.RFC3339Nano, args) if err != nil || et.IsZero() { // Again, if it doesn't seem valid, leave it alone. return true } if c.expiry.IsZero() || et.Before(c.expiry) { c.expiry = et } return false } func (c *caveatSquasher) final() []string { if !c.expiry.IsZero() { c.conds = append(c.conds, checkers.TimeBeforeCaveat(c.expiry).Condition) } if len(c.conds) == 0 { return nil } // Make deterministic and eliminate duplicates. sort.Strings(c.conds) prev := c.conds[0] j := 1 for _, cond := range c.conds[1:] { if cond != prev { c.conds[j] = cond prev = cond j++ } } c.conds = c.conds[:j] return c.conds } macaroon-bakery-3.0.2/bakery/checker_test.go000066400000000000000000000476301464427415500210630ustar00rootroot00000000000000package bakery_test import ( "context" "sort" "strings" "testing" "time" qt "github.com/frankban/quicktest" "gopkg.in/errgo.v1" "gopkg.in/macaroon.v2" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/checkers" ) func TestCapability(t *testing.T) { c := qt.New(t) ts := newService(nil) m := ts.newMacaroon(readOp("something")) // Check that we can exercise the capability directly on the service // with no discharging required. authInfo, err := ts.do(testContext, []macaroon.Slice{m}, readOp("something")) c.Assert(err, qt.IsNil) c.Assert(authInfo, qt.Not(qt.IsNil)) c.Assert(authInfo.Macaroons, qt.HasLen, 1) c.Assert(authInfo.Macaroons[0][0].Id(), qt.DeepEquals, m[0].Id()) c.Assert(authInfo.Used, qt.DeepEquals, []bool{true}) } func TestCapabilityMultipleEntities(t *testing.T) { c := qt.New(t) ts := newService(nil) m := ts.newMacaroon(readOp("e1"), readOp("e2"), readOp("e3")) // Check that we can exercise the capability directly on the service // with no discharging required. _, err := ts.do(testContext, []macaroon.Slice{m}, readOp("e1"), readOp("e2"), readOp("e3")) c.Assert(err, qt.IsNil) // Check that we can exercise the capability to act on a subset of the operations. _, err = ts.do(testContext, []macaroon.Slice{m}, readOp("e2"), readOp("e3")) c.Assert(err, qt.IsNil) _, err = ts.do(testContext, []macaroon.Slice{m}, readOp("e3")) c.Assert(err, qt.IsNil) } func TestMultipleCapabilities(t *testing.T) { c := qt.New(t) ts := newService(nil) // Acquire two capabilities as different users and check // that we can combine them together to do both operations // at once. m1 := ts.newMacaroon(readOp("e1")) m2 := ts.newMacaroon(readOp("e2")) authInfo, err := ts.do(testContext, []macaroon.Slice{m1, m2}, readOp("e1"), readOp("e2")) c.Assert(err, qt.IsNil) c.Assert(authInfo, qt.Not(qt.IsNil)) c.Assert(authInfo.Macaroons, qt.HasLen, 2) c.Assert(authInfo.Used, qt.DeepEquals, []bool{true, true}) } func TestCombineCapabilities(t *testing.T) { c := qt.New(t) ts := newService(nil) // Acquire two capabilities as different users and check // that we can combine them together into a single capability // capable of both operations. m1 := ts.newMacaroon(readOp("e1"), readOp("e3")) m2 := ts.newMacaroon(readOp("e2")) m, err := ts.capability(testContext, []macaroon.Slice{m1, m2}, readOp("e1"), readOp("e2"), readOp("e3")) c.Assert(err, qt.IsNil) _, err = ts.do(testContext, []macaroon.Slice{{m.M()}}, readOp("e1"), readOp("e2"), readOp("e3")) c.Assert(err, qt.IsNil) } func TestCapabilityCombinesFirstPartyCaveats(t *testing.T) { c := qt.New(t) ts := newService(nil) // Acquire two capabilities as different users, add some first party caveats // and combine them together into a single capability // capable of both operations. m1 := ts.newMacaroon(readOp("e1")) m1[0].AddFirstPartyCaveat([]byte("true 1")) m1[0].AddFirstPartyCaveat([]byte("true 2")) m2 := ts.newMacaroon(readOp("e2")) m2[0].AddFirstPartyCaveat([]byte("true 3")) m2[0].AddFirstPartyCaveat([]byte("true 4")) client := newClient(nil) client.addMacaroon(ts, "authz1", m1) client.addMacaroon(ts, "authz2", m2) m, err := client.capability(testContext, ts, readOp("e1"), readOp("e2")) c.Assert(err, qt.IsNil) c.Assert(macaroonConditions(m.M().Caveats(), false), qt.DeepEquals, []string{ "true 1", "true 2", "true 3", "true 4", }) } var firstPartyCaveatSquashingTests = []struct { about string caveats []checkers.Caveat expect []checkers.Caveat }{{ about: "duplicates removed", caveats: []checkers.Caveat{ trueCaveat("1"), trueCaveat("2"), trueCaveat("1"), trueCaveat("2"), trueCaveat("3"), }, expect: []checkers.Caveat{ trueCaveat("1"), trueCaveat("2"), trueCaveat("3"), }, }, { about: "earliest time before", caveats: []checkers.Caveat{ checkers.TimeBeforeCaveat(epoch.Add(24 * time.Hour)), trueCaveat("1"), checkers.TimeBeforeCaveat(epoch.Add(1 * time.Hour)), checkers.TimeBeforeCaveat(epoch.Add(5 * time.Minute)), }, expect: []checkers.Caveat{ checkers.TimeBeforeCaveat(epoch.Add(5 * time.Minute)), trueCaveat("1"), }, }} func TestFirstPartyCaveatSquashing(t *testing.T) { c := qt.New(t) ts := newService(nil) for i, test := range firstPartyCaveatSquashingTests { c.Logf("test %d: %v", i, test.about) // Make a first macaroon with all the required first party caveats. m1 := ts.newMacaroon(readOp("e1")) for _, cond := range resolveCaveats(ts.checker.Namespace(), test.caveats) { err := m1[0].AddFirstPartyCaveat([]byte(cond)) c.Assert(err, qt.Equals, nil) } m2 := ts.newMacaroon(readOp("e2")) err := m2[0].AddFirstPartyCaveat([]byte("notused")) c.Assert(err, qt.Equals, nil) client := newClient(nil) client.addMacaroon(ts, "authz1", m1) client.addMacaroon(ts, "authz2", m2) m3, err := client.capability(testContext, ts, readOp("e1")) c.Assert(err, qt.IsNil) c.Assert(macaroonConditions(m3.M().Caveats(), false), qt.DeepEquals, resolveCaveats(m3.Namespace(), test.expect)) } } func TestAllowDirect(t *testing.T) { c := qt.New(t) ts := newService(nil) client := newClient(nil) client.addMacaroon(ts, "auth1", ts.newMacaroon(readOp("e1"))) client.addMacaroon(ts, "auth2", ts.newMacaroon(readOp("e2"))) ai, err := client.do(testContext, ts, readOp("e1"), readOp("e2")) c.Assert(err, qt.Equals, nil) c.Assert(ai.Macaroons, qt.HasLen, 2) c.Assert(ai.Used, qt.DeepEquals, []bool{true, true}) c.Assert(ai.OpIndexes, qt.DeepEquals, map[bakery.Op]int{ readOp("e1"): 0, readOp("e2"): 1, }) } func TestAllowAlwaysAllowsNoOp(t *testing.T) { c := qt.New(t) ts := newService(nil) client := newClient(nil) _, err := client.do(testContext, ts, bakery.Op{}) c.Assert(err, qt.Equals, nil) } func TestAllowWithInvalidMacaroon(t *testing.T) { c := qt.New(t) ts := newService(nil) client := newClient(nil) m1 := ts.newMacaroon(readOp("e1"), readOp("e2")) m1[0].AddFirstPartyCaveat([]byte("invalid")) m2 := ts.newMacaroon(readOp("e1")) client.addMacaroon(ts, "auth1", m1) client.addMacaroon(ts, "auth2", m2) // Check that we can't do both operations. ai, err := client.do(testContext, ts, readOp("e1"), readOp("e2")) c.Assert(err, qt.ErrorMatches, `caveat "invalid" not satisfied: caveat not recognized`) c.Assert(ai, qt.IsNil) ai, err = client.do(testContext, ts, readOp("e1")) c.Assert(err, qt.Equals, nil) c.Assert(ai.Used, qt.DeepEquals, []bool{false, true}) c.Assert(ai.OpIndexes, qt.DeepEquals, map[bakery.Op]int{ readOp("e1"): 1, }) } func TestAllowed(t *testing.T) { c := qt.New(t) ts := newService(nil) // Get two capabilities with overlapping operations. m1 := ts.newMacaroon(readOp("e1"), readOp("e2")) m2 := ts.newMacaroon(readOp("e2"), readOp("e3")) authInfo, err := ts.checker.Auth(m1, m2).Allowed(context.Background()) c.Assert(err, qt.IsNil) c.Assert(authInfo.Macaroons, qt.HasLen, 2) c.Assert(authInfo.Used, qt.DeepEquals, []bool{true, true}) c.Assert(authInfo.OpIndexes, qt.DeepEquals, map[bakery.Op]int{ readOp("e1"): 0, readOp("e2"): 0, readOp("e3"): 1, }) } func TestAllowWithOpsAuthorizer(t *testing.T) { c := qt.New(t) store := newMacaroonStore(nil) ts := &service{ checker: bakery.NewChecker(bakery.CheckerParams{ Checker: testChecker, OpsAuthorizer: hierarchicalOpsAuthorizer{}, MacaroonVerifier: store, }), store: store, } // Manufacture a macaroon granting access to /user/bob and // everything underneath it (by virtue of the hierarchicalOpsAuthorizer). m := ts.newMacaroon(bakery.Op{ Entity: "path-/user/bob", Action: "*", }) // Check that we can do some operation. _, err := ts.do(testContext, []macaroon.Slice{m}, writeOp("path-/user/bob/foo")) c.Assert(err, qt.Equals, nil) // Check that we can't do an operation on an entity outside the // original operation's purview. _, err = ts.do(testContext, []macaroon.Slice{m}, writeOp("path-/user/alice")) c.Assert(err, qt.ErrorMatches, `permission denied`) } func TestAllowWithOpsAuthorizerAndNoOp(t *testing.T) { c := qt.New(t) store := newMacaroonStore(nil) ts := &service{ checker: bakery.NewChecker(bakery.CheckerParams{ Checker: testChecker, OpsAuthorizer: nopOpsAuthorizer{}, MacaroonVerifier: store, }), store: store, } // Check that we can do a public operation with no operations authorized. _, err := ts.do(testContext, nil, readOp("public")) c.Assert(err, qt.Equals, nil) } func TestOpsAuthorizerError(t *testing.T) { c := qt.New(t) store := newMacaroonStore(nil) ts := &service{ checker: bakery.NewChecker(bakery.CheckerParams{ Checker: testChecker, OpsAuthorizer: errorOpsAuthorizer{"some issue"}, MacaroonVerifier: store, }), store: store, } _, err := ts.do(testContext, nil, readOp("public")) c.Assert(err, qt.ErrorMatches, "some issue") } func TestOpsAuthorizerWithCaveats(t *testing.T) { c := qt.New(t) locator := make(dischargerLocator) store := newMacaroonStore(locator) var discharges []string locator["somewhere"] = &discharger{ key: mustGenerateKey(), locator: locator, checker: bakery.ThirdPartyCaveatCheckerFunc(func(_ context.Context, c *bakery.ThirdPartyCaveatInfo) ([]checkers.Caveat, error) { discharges = append(discharges, string(c.Condition)) return nil, nil }), } opAuth := map[bakery.Op]map[bakery.Op][]checkers.Caveat{ readOp("everywhere1"): { readOp("somewhere1"): {{ Location: "somewhere", Condition: "somewhere1-1", }, { Location: "somewhere", Condition: "somewhere1-2", }}, }, readOp("everywhere2"): { readOp("somewhere2"): {{ Location: "somewhere", Condition: "somewhere2-1", }, { Location: "somewhere", Condition: "somewhere2-2", }}, }, } ts := &service{ checker: bakery.NewChecker(bakery.CheckerParams{ Checker: testChecker, OpsAuthorizer: caveatOpsAuthorizer{opAuth}, MacaroonVerifier: store, }), store: store, } client := newClient(locator) client.addMacaroon(ts, "auth", ts.newMacaroon(readOp("everywhere1"), readOp("everywhere2"))) _, err := client.do(testContext, ts, readOp("somewhere1"), readOp("somewhere2")) c.Assert(err, qt.Equals, nil) sort.Strings(discharges) c.Assert(discharges, qt.DeepEquals, []string{ "somewhere1-1", "somewhere1-2", "somewhere2-1", "somewhere2-2", }) } func TestMacaroonVerifierFatalError(t *testing.T) { c := qt.New(t) // When we get a non-VerificationError error from the // opstore, we don't do any more verification. checker := bakery.NewChecker(bakery.CheckerParams{ MacaroonVerifier: macaroonVerifierWithError{errgo.New("an error")}, }) m, err := macaroon.New(nil, nil, "", macaroon.V2) c.Assert(err, qt.IsNil) _, err = checker.Auth(macaroon.Slice{m}).Allow(testContext, basicOp) c.Assert(err, qt.ErrorMatches, `cannot retrieve macaroon: an error`) } // resolveCaveats resolves all the given caveats with the // given namespace and includes the condition // from each one. It will panic if it finds a third party caveat. func resolveCaveats(ns *checkers.Namespace, caveats []checkers.Caveat) []string { conds := make([]string, len(caveats)) for i, cav := range caveats { if cav.Location != "" { panic("found unexpected third party caveat") } conds[i] = ns.ResolveCaveat(cav).Condition } return conds } func macaroonConditions(caveats []macaroon.Caveat, allowThird bool) []string { conds := make([]string, len(caveats)) for i, cav := range caveats { if cav.Location != "" { if !allowThird { panic("found unexpected third party caveat") } continue } conds[i] = string(cav.Id) } return conds } func readOp(entity string) bakery.Op { return bakery.Op{ Entity: entity, Action: "read", } } func writeOp(entity string) bakery.Op { return bakery.Op{ Entity: entity, Action: "write", } } // service represents a service that requires authorization. // Clients can make requests to the service to perform operations // and may receive a macaroon to discharge if the authorization // process requires it. type service struct { checker *bakery.Checker store *macaroonStore } func newService(locator bakery.ThirdPartyLocator) *service { store := newMacaroonStore(locator) return &service{ checker: bakery.NewChecker(bakery.CheckerParams{ Checker: testChecker, MacaroonVerifier: store, }), store: store, } } // do makes a request to the service to perform the given operations // using the given macaroons for authorization. // It may return a dischargeRequiredError containing a macaroon // that needs to be discharged. func (svc *service) do(ctx context.Context, ms []macaroon.Slice, ops ...bakery.Op) (*bakery.AuthInfo, error) { authInfo, err := svc.checker.Auth(ms...).Allow(ctx, ops...) return authInfo, svc.maybeDischargeRequiredError(err) } // newMacaroon returns a macaroon with no caveats that allows the given operations. func (svc *service) newMacaroon(ops ...bakery.Op) macaroon.Slice { m, err := svc.store.NewMacaroon(testContext, ops, nil, svc.checker.Namespace()) if err != nil { panic(err) } return macaroon.Slice{m.M()} } // capability checks that the given macaroons have authorization for the // given operations and, if so, returns a macaroon that has that authorization. func (svc *service) capability(ctx context.Context, ms []macaroon.Slice, ops ...bakery.Op) (*bakery.Macaroon, error) { ai, err := svc.checker.Auth(ms...).Allow(ctx, ops...) if err != nil { return nil, svc.maybeDischargeRequiredError(err) } m, err := svc.store.NewMacaroon(ctx, ops, nil, svc.checker.Namespace()) if err != nil { return nil, errgo.Mask(err) } for _, cond := range ai.Conditions() { if err := m.M().AddFirstPartyCaveat([]byte(cond)); err != nil { return nil, errgo.Mask(err) } } return m, nil } func (svc *service) maybeDischargeRequiredError(err error) error { derr, ok := errgo.Cause(err).(*bakery.DischargeRequiredError) if !ok { return errgo.Mask(err) } m, err := svc.store.NewMacaroon(testContext, derr.Ops, derr.Caveats, svc.checker.Namespace()) if err != nil { return errgo.Mask(err) } return &dischargeRequiredError{ name: "authz", m: m, } } type discharger struct { key *bakery.KeyPair locator bakery.ThirdPartyLocator checker bakery.ThirdPartyCaveatChecker } type dischargeRequiredError struct { name string m *bakery.Macaroon } func (*dischargeRequiredError) Error() string { return "discharge required" } func (d *discharger) discharge(ctx context.Context, cav macaroon.Caveat, payload []byte) (*bakery.Macaroon, error) { m, err := bakery.Discharge(ctx, bakery.DischargeParams{ Id: cav.Id, Caveat: payload, Key: d.key, Checker: d.checker, Locator: d.locator, }) if err != nil { return nil, errgo.Mask(err) } return m, nil } type dischargerLocator map[string]*discharger // ThirdPartyInfo implements the bakery.ThirdPartyLocator interface. func (l dischargerLocator) ThirdPartyInfo(ctx context.Context, loc string) (bakery.ThirdPartyInfo, error) { d, ok := l[loc] if !ok { return bakery.ThirdPartyInfo{}, bakery.ErrNotFound } return bakery.ThirdPartyInfo{ PublicKey: d.key.Public, Version: bakery.LatestVersion, }, nil } type nopOpsAuthorizer struct{} func (nopOpsAuthorizer) AuthorizeOps(ctx context.Context, authorizedOp bakery.Op, queryOps []bakery.Op) ([]bool, []checkers.Caveat, error) { if authorizedOp != bakery.NoOp { return nil, nil, nil } authed := make([]bool, len(queryOps)) for i, op := range queryOps { if op.Entity == "public" { authed[i] = true } } return authed, nil, nil } type hierarchicalOpsAuthorizer struct{} func (hierarchicalOpsAuthorizer) AuthorizeOps(ctx context.Context, authorizedOp bakery.Op, queryOps []bakery.Op) ([]bool, []checkers.Caveat, error) { ok := make([]bool, len(queryOps)) for i, op := range queryOps { if isParentPathEntity(authorizedOp.Entity, op.Entity) && (authorizedOp.Action == op.Action || authorizedOp.Action == "*") { ok[i] = true } } return ok, nil, nil } type errorOpsAuthorizer struct { err string } func (a errorOpsAuthorizer) AuthorizeOps(ctx context.Context, authorizedOp bakery.Op, queryOps []bakery.Op) ([]bool, []checkers.Caveat, error) { return nil, nil, errgo.New(a.err) } type caveatOpsAuthorizer struct { // authOps holds a map from authorizing op to the // ops that it authorizes to the caveats associated with that. authOps map[bakery.Op]map[bakery.Op][]checkers.Caveat } func (a caveatOpsAuthorizer) AuthorizeOps(ctx context.Context, authorizedOp bakery.Op, queryOps []bakery.Op) ([]bool, []checkers.Caveat, error) { authed := make([]bool, len(queryOps)) var caveats []checkers.Caveat for i, op := range queryOps { if authCaveats, ok := a.authOps[authorizedOp][op]; ok { caveats = append(caveats, authCaveats...) authed[i] = true } } return authed, caveats, nil } // isParentPathEntity reports whether both entity1 and entity2 // represent paths and entity1 is a parent of entity2. func isParentPathEntity(entity1, entity2 string) bool { path1, path2 := strings.TrimPrefix(entity1, "path-"), strings.TrimPrefix(entity2, "path-") if len(path1) == len(entity1) || len(path2) == len(entity2) { return false } if !strings.HasPrefix(path2, path1) { return false } if len(path1) == len(path2) { return true } return path2[len(path1)] == '/' } type client struct { key *bakery.KeyPair macaroons map[*service]map[string]macaroon.Slice dischargers dischargerLocator } func newClient(dischargers dischargerLocator) *client { return &client{ key: mustGenerateKey(), dischargers: dischargers, // macaroons holds the macaroons applicable to each service. // This is the test equivalent of the cookie jar used by httpbakery. macaroons: make(map[*service]map[string]macaroon.Slice), } } const maxRetries = 3 // do performs a set of operations on the given service. // It includes all the macaroons in c.macaroons[svc] as authorization // information on the request. func (c *client) do(ctx context.Context, svc *service, ops ...bakery.Op) (*bakery.AuthInfo, error) { var authInfo *bakery.AuthInfo err := c.doFunc(ctx, svc, func(ms []macaroon.Slice) (err error) { authInfo, err = svc.do(ctx, ms, ops...) return }) return authInfo, err } // capability returns a capability macaroon for the given operations. func (c *client) capability(ctx context.Context, svc *service, ops ...bakery.Op) (*bakery.Macaroon, error) { var m *bakery.Macaroon err := c.doFunc(ctx, svc, func(ms []macaroon.Slice) (err error) { m, err = svc.capability(ctx, ms, ops...) return }) return m, err } func (c *client) doFunc(ctx context.Context, svc *service, f func(ms []macaroon.Slice) error) error { for i := 0; i < maxRetries; i++ { err := f(c.requestMacaroons(svc)) derr, ok := errgo.Cause(err).(*dischargeRequiredError) if !ok { return err } ms, err := c.dischargeAll(ctx, derr.m) if err != nil { return errgo.Mask(err) } c.addMacaroon(svc, derr.name, ms) } return errgo.New("discharge failed too many times") } func (c *client) clearMacaroons(svc *service) { if svc == nil { c.macaroons = make(map[*service]map[string]macaroon.Slice) return } delete(c.macaroons, svc) } func (c *client) addMacaroon(svc *service, name string, m macaroon.Slice) { if c.macaroons[svc] == nil { c.macaroons[svc] = make(map[string]macaroon.Slice) } c.macaroons[svc][name] = m } func (c *client) requestMacaroons(svc *service) []macaroon.Slice { mmap := c.macaroons[svc] // Put all the macaroons in the slice ordered by key // so that we have deterministic behaviour in the tests. names := make([]string, 0, len(mmap)) for name := range mmap { names = append(names, name) } sort.Strings(names) ms := make([]macaroon.Slice, len(names)) for i, name := range names { ms[i] = mmap[name] } return ms } func (c *client) dischargeAll(ctx context.Context, m *bakery.Macaroon) (macaroon.Slice, error) { return bakery.DischargeAll(ctx, m, func(ctx context.Context, cav macaroon.Caveat, payload []byte) (*bakery.Macaroon, error) { d := c.dischargers[cav.Location] if d == nil { return nil, errgo.Newf("third party discharger %q not found", cav.Location) } return d.discharge(ctx, cav, payload) }) } macaroon-bakery-3.0.2/bakery/checkers/000077500000000000000000000000001464427415500176465ustar00rootroot00000000000000macaroon-bakery-3.0.2/bakery/checkers/checkers.go000066400000000000000000000166031464427415500217720ustar00rootroot00000000000000// The checkers package provides some standard first-party // caveat checkers and some primitives for combining them. package checkers import ( "context" "fmt" "sort" "strings" "gopkg.in/errgo.v1" ) // StdNamespace holds the URI of the standard checkers schema. const StdNamespace = "std" // Constants for all the standard caveat conditions. // First and third party caveat conditions are both defined here, // even though notionally they exist in separate name spaces. const ( CondDeclared = "declared" CondTimeBefore = "time-before" CondError = "error" ) const ( CondNeedDeclared = "need-declared" ) // Func is the type of a function used by Checker to check a caveat. The // cond parameter will hold the caveat condition including any namespace // prefix; the arg parameter will hold any additional caveat argument // text. type Func func(ctx context.Context, cond, arg string) error // CheckerInfo holds information on a registered checker. type CheckerInfo struct { // Check holds the actual checker function. Check Func // Prefix holds the prefix for the checker condition. Prefix string // Name holds the name of the checker condition. Name string // Namespace holds the namespace URI for the checker's // schema. Namespace string } var allCheckers = map[string]Func{ CondTimeBefore: checkTimeBefore, CondDeclared: checkDeclared, CondError: checkError, } // NewEmpty returns a checker using the given namespace // that has no registered checkers. // If ns is nil, a new one will be created. func NewEmpty(ns *Namespace) *Checker { if ns == nil { ns = NewNamespace(nil) } return &Checker{ namespace: ns, checkers: make(map[string]CheckerInfo), } } // RegisterStd registers all the standard checkers in the given checker. // If not present already, the standard checkers schema (StdNamespace) is // added to the checker's namespace with an empty prefix. func RegisterStd(c *Checker) { c.namespace.Register(StdNamespace, "") for cond, check := range allCheckers { c.Register(cond, StdNamespace, check) } } // New returns a checker with all the standard caveats checkers registered. // If ns is nil, a new one will be created. // The standard namespace is also added to ns if not present. func New(ns *Namespace) *Checker { c := NewEmpty(ns) RegisterStd(c) return c } // Checker holds a set of checkers for first party caveats. // It implements bakery.CheckFirstParty caveat. type Checker struct { namespace *Namespace checkers map[string]CheckerInfo } // Register registers the given condition in the given namespace URI // to be checked with the given check function. // It will panic if the namespace is not registered or // if the condition has already been registered. func (c *Checker) Register(cond, uri string, check Func) { if check == nil { panic(fmt.Errorf("nil check function registered for namespace %q when registering condition %q", uri, cond)) } prefix, ok := c.namespace.Resolve(uri) if !ok { panic(fmt.Errorf("no prefix registered for namespace %q when registering condition %q", uri, cond)) } if prefix == "" && strings.Contains(cond, ":") { panic(fmt.Errorf("caveat condition %q in namespace %q contains a colon but its prefix is empty", cond, uri)) } fullCond := ConditionWithPrefix(prefix, cond) if info, ok := c.checkers[fullCond]; ok { panic(fmt.Errorf("checker for %q (namespace %q) already registered in namespace %q", fullCond, uri, info.Namespace)) } c.checkers[fullCond] = CheckerInfo{ Check: check, Namespace: uri, Name: cond, Prefix: prefix, } } // Info returns information on all the registered checkers, sorted by namespace // and then name. func (c *Checker) Info() []CheckerInfo { checkers := make([]CheckerInfo, 0, len(c.checkers)) for _, c := range c.checkers { checkers = append(checkers, c) } sort.Sort(checkerInfoByName(checkers)) return checkers } // Namespace returns the namespace associated with the // checker. It implements bakery.FirstPartyCaveatChecker.Namespace. func (c *Checker) Namespace() *Namespace { return c.namespace } // CheckFirstPartyCaveat implements bakery.FirstPartyCaveatChecker // by checking the caveat against all registered caveats conditions. func (c *Checker) CheckFirstPartyCaveat(ctx context.Context, cav string) error { cond, arg, err := ParseCaveat(cav) if err != nil { // If we can't parse it, perhaps it's in some other format, // return a not-recognised error. return errgo.WithCausef(err, ErrCaveatNotRecognized, "cannot parse caveat %q", cav) } cf, ok := c.checkers[cond] if !ok { return errgo.NoteMask(ErrCaveatNotRecognized, fmt.Sprintf("caveat %q not satisfied", cav), errgo.Any) } if err := cf.Check(ctx, cond, arg); err != nil { return errgo.NoteMask(err, fmt.Sprintf("caveat %q not satisfied", cav), errgo.Any) } return nil } var errBadCaveat = errgo.New("bad caveat") func checkError(ctx context.Context, _, arg string) error { return errBadCaveat } // ErrCaveatNotRecognized is the cause of errors returned // from caveat checkers when the caveat was not // recognized. var ErrCaveatNotRecognized = errgo.New("caveat not recognized") // Caveat represents a condition that must be true for a check to // complete successfully. If Location is non-empty, the caveat must be // discharged by a third party at the given location. // The Namespace field holds the namespace URI of the // condition - if it is non-empty, it will be converted to // a namespace prefix before adding to the macaroon. type Caveat struct { Condition string Namespace string Location string } // Condition builds a caveat condition from the given name and argument. func Condition(name, arg string) string { if arg == "" { return name } return name + " " + arg } func firstParty(name, arg string) Caveat { return Caveat{ Condition: Condition(name, arg), Namespace: StdNamespace, } } // ParseCaveat parses a caveat into an identifier, identifying the // checker that should be used, and the argument to the checker (the // rest of the string). // // The identifier is taken from all the characters before the first // space character. func ParseCaveat(cav string) (cond, arg string, err error) { if cav == "" { return "", "", fmt.Errorf("empty caveat") } i := strings.IndexByte(cav, ' ') if i < 0 { return cav, "", nil } if i == 0 { return "", "", fmt.Errorf("caveat starts with space character") } return cav[0:i], cav[i+1:], nil } // ErrorCaveatf returns a caveat that will never be satisfied, holding // the given fmt.Sprintf formatted text as the text of the caveat. // // This should only be used for highly unusual conditions that are never // expected to happen in practice, such as a malformed key that is // conventionally passed as a constant. It's not a panic but you should // only use it in cases where a panic might possibly be appropriate. // // This mechanism means that caveats can be created without error // checking and a later systematic check at a higher level (in the // bakery package) can produce an error instead. func ErrorCaveatf(f string, a ...interface{}) Caveat { return firstParty(CondError, fmt.Sprintf(f, a...)) } type checkerInfoByName []CheckerInfo func (c checkerInfoByName) Less(i, j int) bool { info0, info1 := &c[i], &c[j] if info0.Namespace != info1.Namespace { return info0.Namespace < info1.Namespace } return info0.Name < info1.Name } func (c checkerInfoByName) Swap(i, j int) { c[i], c[j] = c[j], c[i] } func (c checkerInfoByName) Len() int { return len(c) } macaroon-bakery-3.0.2/bakery/checkers/checkers_test.go000066400000000000000000000325421464427415500230310ustar00rootroot00000000000000package checkers_test import ( "context" "fmt" "testing" "time" qt "github.com/frankban/quicktest" "gopkg.in/errgo.v1" "gopkg.in/macaroon.v2" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/checkers" ) // A frozen time for the tests. var now = time.Date(2006, time.January, 2, 15, 4, 5, int(123*time.Millisecond), time.UTC) // testClock is a Clock implementation that always returns the above time. type testClock struct{} func (testClock) Now() time.Time { return now } // FirstPartyCaveatChecker is declared here so we can avoid a cyclic // dependency on the bakery. type FirstPartyCaveatChecker interface { CheckFirstPartyCaveat(ctx context.Context, caveat string) error } type checkTest struct { caveat string expectError string expectCause func(err error) bool } var isCaveatNotRecognized = errgo.Is(checkers.ErrCaveatNotRecognized) var checkerTests = []struct { about string addContext func(context.Context, *checkers.Namespace) context.Context checks []checkTest }{{ about: "nothing in context, no extra checkers", checks: []checkTest{{ caveat: "something", expectError: `caveat "something" not satisfied: caveat not recognized`, expectCause: isCaveatNotRecognized, }, { caveat: "", expectError: `cannot parse caveat "": empty caveat`, expectCause: isCaveatNotRecognized, }, { caveat: " hello", expectError: `cannot parse caveat " hello": caveat starts with space character`, expectCause: isCaveatNotRecognized, }}, }, { about: "one failed caveat", checks: []checkTest{{ caveat: "t:a aval", }, { caveat: "t:b bval", }, { caveat: "t:a wrong", expectError: `caveat "t:a wrong" not satisfied: wrong arg`, expectCause: errgo.Is(errWrongArg), }}, }, { about: "time from clock", addContext: func(ctx context.Context, _ *checkers.Namespace) context.Context { return checkers.ContextWithClock(ctx, testClock{}) }, checks: []checkTest{{ caveat: checkers.TimeBeforeCaveat(now.Add(1)).Condition, }, { caveat: checkers.TimeBeforeCaveat(now).Condition, expectError: `caveat "time-before 2006-01-02T15:04:05.123Z" not satisfied: macaroon has expired`, }, { caveat: checkers.TimeBeforeCaveat(now.Add(-1)).Condition, expectError: `caveat "time-before 2006-01-02T15:04:05.122999999Z" not satisfied: macaroon has expired`, }, { caveat: `time-before bad-date`, expectError: `caveat "time-before bad-date" not satisfied: parsing time "bad-date" as "2006-01-02T15:04:05.999999999Z07:00": cannot parse "bad-date" as "2006"`, }, { caveat: checkers.TimeBeforeCaveat(now).Condition + " ", expectError: `caveat "time-before 2006-01-02T15:04:05.123Z " not satisfied: parsing time "2006-01-02T15:04:05.123Z ": extra text: "? "?`, }}, }, { about: "real time", checks: []checkTest{{ caveat: checkers.TimeBeforeCaveat(time.Date(2010, time.January, 1, 0, 0, 0, 0, time.UTC)).Condition, expectError: `caveat "time-before 2010-01-01T00:00:00Z" not satisfied: macaroon has expired`, }, { caveat: checkers.TimeBeforeCaveat(time.Date(3000, time.January, 1, 0, 0, 0, 0, time.UTC)).Condition, }}, }, { about: "declared, no entries", checks: []checkTest{{ caveat: checkers.DeclaredCaveat("a", "aval").Condition, expectError: `caveat "declared a aval" not satisfied: got a=null, expected "aval"`, }, { caveat: checkers.CondDeclared, expectError: `caveat "declared" not satisfied: declared caveat has no value`, }}, }, { about: "declared, some entries", addContext: func(ctx context.Context, ns *checkers.Namespace) context.Context { m, _ := macaroon.New([]byte("k"), []byte("id"), "", macaroon.LatestVersion) add := func(attr, val string) { cav := ns.ResolveCaveat(checkers.DeclaredCaveat(attr, val)) err := m.AddFirstPartyCaveat([]byte(cav.Condition)) if err != nil { panic(err) } } add("a", "aval") add("b", "bval") add("spc", " a b") return checkers.ContextWithMacaroons(ctx, ns, macaroon.Slice{m}) }, checks: []checkTest{{ caveat: checkers.DeclaredCaveat("a", "aval").Condition, }, { caveat: checkers.DeclaredCaveat("b", "bval").Condition, }, { caveat: checkers.DeclaredCaveat("spc", " a b").Condition, }, { caveat: checkers.DeclaredCaveat("a", "bval").Condition, expectError: `caveat "declared a bval" not satisfied: got a="aval", expected "bval"`, }, { caveat: checkers.DeclaredCaveat("a", " aval").Condition, expectError: `caveat "declared a aval" not satisfied: got a="aval", expected " aval"`, }, { caveat: checkers.DeclaredCaveat("spc", "a b").Condition, expectError: `caveat "declared spc a b" not satisfied: got spc=" a b", expected "a b"`, }, { caveat: checkers.DeclaredCaveat("", "a b").Condition, expectError: `caveat "error invalid caveat 'declared' key \\"\\"" not satisfied: bad caveat`, }, { caveat: checkers.DeclaredCaveat("a b", "a b").Condition, expectError: `caveat "error invalid caveat 'declared' key \\"a b\\"" not satisfied: bad caveat`, }}, }, { about: "error caveat", checks: []checkTest{{ caveat: checkers.ErrorCaveatf("").Condition, expectError: `caveat "error" not satisfied: bad caveat`, }, { caveat: checkers.ErrorCaveatf("something %d", 134).Condition, expectError: `caveat "error something 134" not satisfied: bad caveat`, }}, }} var errWrongArg = errgo.New("wrong arg") // argChecker returns a checker function that checks // that the caveat condition is checkArg. func argChecker(c *qt.C, expectCond, checkArg string) checkers.Func { return func(_ context.Context, cond, arg string) error { c.Assert(cond, qt.Equals, expectCond) if arg != checkArg { return errWrongArg } return nil } } func TestCheckers(t *testing.T) { c := qt.New(t) checker := checkers.New(nil) checker.Namespace().Register("testns", "t") checker.Register("a", "testns", argChecker(c, "t:a", "aval")) checker.Register("b", "testns", argChecker(c, "t:b", "bval")) for i, test := range checkerTests { c.Logf("test %d: %s", i, test.about) ctx := context.Background() if test.addContext != nil { ctx = test.addContext(ctx, checker.Namespace()) } for j, check := range test.checks { c.Logf("\tcheck %d", j) err := checker.CheckFirstPartyCaveat(ctx, check.caveat) if check.expectError != "" { c.Assert(err, qt.ErrorMatches, check.expectError) if check.expectCause == nil { check.expectCause = errgo.Any } c.Assert(check.expectCause(errgo.Cause(err)), qt.Equals, true) } else { c.Assert(err, qt.IsNil) } } } } var inferDeclaredTests = []struct { about string caveats [][]checkers.Caveat expect map[string]string namespace map[string]string }{{ about: "no macaroons", expect: map[string]string{}, }, { about: "single macaroon with one declaration", caveats: [][]checkers.Caveat{{{ Condition: "declared foo bar", }}}, expect: map[string]string{ "foo": "bar", }, }, { about: "only one argument to declared", caveats: [][]checkers.Caveat{{{ Condition: "declared foo", }}}, expect: map[string]string{}, }, { about: "spaces in value", caveats: [][]checkers.Caveat{{{ Condition: "declared foo bar bloggs", }}}, expect: map[string]string{ "foo": "bar bloggs", }, }, { about: "attribute with declared prefix", caveats: [][]checkers.Caveat{{{ Condition: "declaredccf foo", }}}, expect: map[string]string{}, }, { about: "several macaroons with different declares", caveats: [][]checkers.Caveat{{ checkers.DeclaredCaveat("a", "aval"), checkers.DeclaredCaveat("b", "bval"), }, { checkers.DeclaredCaveat("c", "cval"), checkers.DeclaredCaveat("d", "dval"), }}, expect: map[string]string{ "a": "aval", "b": "bval", "c": "cval", "d": "dval", }, }, { about: "duplicate values", caveats: [][]checkers.Caveat{{ checkers.DeclaredCaveat("a", "aval"), checkers.DeclaredCaveat("a", "aval"), checkers.DeclaredCaveat("b", "bval"), }, { checkers.DeclaredCaveat("a", "aval"), checkers.DeclaredCaveat("b", "bval"), checkers.DeclaredCaveat("c", "cval"), checkers.DeclaredCaveat("d", "dval"), }}, expect: map[string]string{ "a": "aval", "b": "bval", "c": "cval", "d": "dval", }, }, { about: "conflicting values", caveats: [][]checkers.Caveat{{ checkers.DeclaredCaveat("a", "aval"), checkers.DeclaredCaveat("a", "conflict"), checkers.DeclaredCaveat("b", "bval"), }, { checkers.DeclaredCaveat("a", "conflict"), checkers.DeclaredCaveat("b", "another conflict"), checkers.DeclaredCaveat("c", "cval"), checkers.DeclaredCaveat("d", "dval"), }}, expect: map[string]string{ "c": "cval", "d": "dval", }, }, { about: "third party caveats ignored", caveats: [][]checkers.Caveat{{{ Condition: "declared a no conflict", Location: "location", }, checkers.DeclaredCaveat("a", "aval"), }}, expect: map[string]string{ "a": "aval", }, }, { about: "unparseable caveats ignored", caveats: [][]checkers.Caveat{{{ Condition: " bad", }, checkers.DeclaredCaveat("a", "aval"), }}, expect: map[string]string{ "a": "aval", }, }, { about: "infer with namespace", namespace: map[string]string{ checkers.StdNamespace: "", "testns": "t", }, caveats: [][]checkers.Caveat{{ checkers.DeclaredCaveat("a", "aval"), // A declared caveat from a different namespace doesn't // interfere. caveatWithNamespace(checkers.DeclaredCaveat("a", "bval"), "testns"), }}, expect: map[string]string{ "a": "aval", }, }} func caveatWithNamespace(cav checkers.Caveat, uri string) checkers.Caveat { cav.Namespace = uri return cav } func TestInferDeclared(t *testing.T) { c := qt.New(t) for i, test := range inferDeclaredTests { if test.namespace == nil { test.namespace = map[string]string{ checkers.StdNamespace: "", } } ns := checkers.NewNamespace(test.namespace) c.Logf("test %d: %s", i, test.about) ms := make(macaroon.Slice, len(test.caveats)) for i, caveats := range test.caveats { m, err := macaroon.New(nil, []byte(fmt.Sprint(i)), "", macaroon.LatestVersion) c.Assert(err, qt.IsNil) for _, cav := range caveats { cav = ns.ResolveCaveat(cav) if cav.Location == "" { m.AddFirstPartyCaveat([]byte(cav.Condition)) } else { m.AddThirdPartyCaveat(nil, []byte(cav.Condition), cav.Location) } } ms[i] = m } c.Assert(checkers.InferDeclared(nil, ms), qt.DeepEquals, test.expect) } } func TestRegisterNilFuncPanics(t *testing.T) { c := qt.New(t) checker := checkers.New(nil) c.Assert(func() { checker.Register("x", checkers.StdNamespace, nil) }, qt.PanicMatches, `nil check function registered for namespace ".*" when registering condition "x"`) } func TestRegisterNoRegisteredNamespace(t *testing.T) { c := qt.New(t) checker := checkers.New(nil) c.Assert(func() { checker.Register("x", "testns", succeed) }, qt.PanicMatches, `no prefix registered for namespace "testns" when registering condition "x"`) } func TestRegisterEmptyPrefixConditionWithColon(t *testing.T) { c := qt.New(t) checker := checkers.New(nil) checker.Namespace().Register("testns", "") c.Assert(func() { checker.Register("x:y", "testns", succeed) }, qt.PanicMatches, `caveat condition "x:y" in namespace "testns" contains a colon but its prefix is empty`) } func TestRegisterTwiceSameNamespace(t *testing.T) { c := qt.New(t) checker := checkers.New(nil) checker.Namespace().Register("testns", "t") checker.Register("x", "testns", succeed) c.Assert(func() { checker.Register("x", "testns", succeed) }, qt.PanicMatches, `checker for "t:x" \(namespace "testns"\) already registered in namespace "testns"`) } func TestRegisterTwiceDifferentNamespace(t *testing.T) { c := qt.New(t) checker := checkers.New(nil) checker.Namespace().Register("testns", "t") checker.Namespace().Register("otherns", "t") checker.Register("x", "testns", succeed) c.Assert(func() { checker.Register("x", "otherns", succeed) }, qt.PanicMatches, `checker for "t:x" \(namespace "otherns"\) already registered in namespace "testns"`) } func TestCheckerInfo(t *testing.T) { c := qt.New(t) checker := checkers.NewEmpty(nil) checker.Namespace().Register("one", "t") checker.Namespace().Register("two", "t") checker.Namespace().Register("three", "") checker.Namespace().Register("four", "s") var calledVal string register := func(name, ns string) { checker.Register(name, ns, func(ctx context.Context, cond, arg string) error { calledVal = name + " " + ns return nil }) } register("x", "one") register("y", "one") register("z", "two") register("a", "two") register("something", "three") register("other", "three") register("xxx", "four") expect := []checkers.CheckerInfo{{ Namespace: "four", Name: "xxx", Prefix: "s", }, { Namespace: "one", Name: "x", Prefix: "t", }, { Namespace: "one", Name: "y", Prefix: "t", }, { Namespace: "three", Name: "other", Prefix: "", }, { Namespace: "three", Name: "something", Prefix: "", }, { Namespace: "two", Name: "a", Prefix: "t", }, { Namespace: "two", Name: "z", Prefix: "t", }} infos := checker.Info() // We can't use DeepEqual on functions so check that the right functions are // there by calling them, then set them to nil. c.Assert(infos, qt.HasLen, len(expect)) for i := range infos { info := &infos[i] calledVal = "" info.Check(nil, "", "") c.Check(calledVal, qt.Equals, expect[i].Name+" "+expect[i].Namespace, qt.Commentf("index %d", i)) info.Check = nil } c.Assert(infos, qt.DeepEquals, expect) } func succeed(ctx context.Context, cond, arg string) error { return nil } macaroon-bakery-3.0.2/bakery/checkers/declared.go000066400000000000000000000102241464427415500217370ustar00rootroot00000000000000package checkers import ( "context" "strings" "gopkg.in/errgo.v1" "gopkg.in/macaroon.v2" ) type macaroonsKey struct{} type macaroonsValue struct { ns *Namespace ms macaroon.Slice } // ContextWithMacaroons returns the given context associated with a // macaroon slice and the name space to use to interpret caveats in // the macaroons. func ContextWithMacaroons(ctx context.Context, ns *Namespace, ms macaroon.Slice) context.Context { return context.WithValue(ctx, macaroonsKey{}, macaroonsValue{ ns: ns, ms: ms, }) } // MacaroonsFromContext returns the namespace and macaroons associated // with the context by ContextWithMacaroons. This can be used to // implement "structural" first-party caveats that are predicated on // the macaroons being validated. func MacaroonsFromContext(ctx context.Context) (*Namespace, macaroon.Slice) { v, _ := ctx.Value(macaroonsKey{}).(macaroonsValue) return v.ns, v.ms } // DeclaredCaveat returns a "declared" caveat asserting that the given key is // set to the given value. If a macaroon has exactly one first party // caveat asserting the value of a particular key, then InferDeclared // will be able to infer the value, and then DeclaredChecker will allow // the declared value if it has the value specified here. // // If the key is empty or contains a space, DeclaredCaveat // will return an error caveat. func DeclaredCaveat(key string, value string) Caveat { if strings.Contains(key, " ") || key == "" { return ErrorCaveatf("invalid caveat 'declared' key %q", key) } return firstParty(CondDeclared, key+" "+value) } // NeedDeclaredCaveat returns a third party caveat that // wraps the provided third party caveat and requires // that the third party must add "declared" caveats for // all the named keys. // TODO(rog) namespaces in third party caveats? func NeedDeclaredCaveat(cav Caveat, keys ...string) Caveat { if cav.Location == "" { return ErrorCaveatf("need-declared caveat is not third-party") } return Caveat{ Location: cav.Location, Condition: CondNeedDeclared + " " + strings.Join(keys, ",") + " " + cav.Condition, } } func checkDeclared(ctx context.Context, _, arg string) error { parts := strings.SplitN(arg, " ", 2) if len(parts) != 2 { return errgo.Newf("declared caveat has no value") } ns, ms := MacaroonsFromContext(ctx) attrs := InferDeclared(ns, ms) val, ok := attrs[parts[0]] if !ok { return errgo.Newf("got %s=null, expected %q", parts[0], parts[1]) } if val != parts[1] { return errgo.Newf("got %s=%q, expected %q", parts[0], val, parts[1]) } return nil } // InferDeclared retrieves any declared information from // the given macaroons and returns it as a key-value map. // // Information is declared with a first party caveat as created // by DeclaredCaveat. // // If there are two caveats that declare the same key with // different values, the information is omitted from the map. // When the caveats are later checked, this will cause the // check to fail. func InferDeclared(ns *Namespace, ms macaroon.Slice) map[string]string { var conditions []string for _, m := range ms { for _, cav := range m.Caveats() { if cav.Location == "" { conditions = append(conditions, string(cav.Id)) } } } return InferDeclaredFromConditions(ns, conditions) } // InferDeclaredFromConditions is like InferDeclared except that // it is passed a set of first party caveat conditions rather than a set of macaroons. func InferDeclaredFromConditions(ns *Namespace, conds []string) map[string]string { var conflicts []string // If we can't resolve that standard namespace, then we'll look for // just bare "declared" caveats which will work OK for legacy // macaroons with no namespace. prefix, _ := ns.Resolve(StdNamespace) declaredCond := prefix + CondDeclared info := make(map[string]string) for _, cond := range conds { name, rest, _ := ParseCaveat(cond) if name != declaredCond { continue } parts := strings.SplitN(rest, " ", 2) if len(parts) != 2 { continue } key, val := parts[0], parts[1] if oldVal, ok := info[key]; ok && oldVal != val { conflicts = append(conflicts, key) continue } info[key] = val } for _, key := range conflicts { delete(info, key) } return info } macaroon-bakery-3.0.2/bakery/checkers/namespace.go000066400000000000000000000142631464427415500221370ustar00rootroot00000000000000package checkers import ( "sort" "strings" "unicode" "unicode/utf8" "gopkg.in/errgo.v1" ) // Namespace holds maps from schema URIs to the // prefixes that are used to encode them in first party // caveats. Several different URIs may map to the same // prefix - this is usual when several different backwardly // compatible schema versions are registered. type Namespace struct { uriToPrefix map[string]string } // Equal reports whether ns2 encodes the same namespace // as the receiver. func (ns1 *Namespace) Equal(ns2 *Namespace) bool { if ns1 == ns2 || ns1 == nil || ns2 == nil { return ns1 == ns2 } if len(ns1.uriToPrefix) != len(ns2.uriToPrefix) { return false } for k, v := range ns1.uriToPrefix { if ns2.uriToPrefix[k] != v { return false } } return true } // NewNamespace returns a new namespace with the // given initial contents. It will panic if any of the // URI keys or their associated prefix are invalid // (see IsValidSchemaURI and IsValidPrefix). func NewNamespace(uriToPrefix map[string]string) *Namespace { ns := &Namespace{ uriToPrefix: make(map[string]string), } for uri, prefix := range uriToPrefix { ns.Register(uri, prefix) } return ns } // String returns the namespace representation as returned by // ns.MarshalText. func (ns *Namespace) String() string { data, _ := ns.MarshalText() return string(data) } // MarshalText implements encoding.TextMarshaler by // returning all the elements in the namespace sorted by // URI, joined to the associated prefix with a colon and // separated with spaces. func (ns *Namespace) MarshalText() ([]byte, error) { if ns == nil || len(ns.uriToPrefix) == 0 { return nil, nil } uris := make([]string, 0, len(ns.uriToPrefix)) dataLen := 0 for uri, prefix := range ns.uriToPrefix { uris = append(uris, uri) dataLen += len(uri) + 1 + len(prefix) + 1 } sort.Strings(uris) data := make([]byte, 0, dataLen) for i, uri := range uris { if i > 0 { data = append(data, ' ') } data = append(data, uri...) data = append(data, ':') data = append(data, ns.uriToPrefix[uri]...) } return data, nil } func (ns *Namespace) UnmarshalText(data []byte) error { uriToPrefix := make(map[string]string) elems := strings.Fields(string(data)) for _, elem := range elems { i := strings.LastIndex(elem, ":") if i == -1 { return errgo.Newf("no colon in namespace field %q", elem) } uri, prefix := elem[0:i], elem[i+1:] if !IsValidSchemaURI(uri) { // Currently this can't happen because the only invalid URIs // are those which contain a space return errgo.Newf("invalid URI %q in namespace field %q", uri, elem) } if !IsValidPrefix(prefix) { return errgo.Newf("invalid prefix %q in namespace field %q", prefix, elem) } if _, ok := uriToPrefix[uri]; ok { return errgo.Newf("duplicate URI %q in namespace %q", uri, data) } uriToPrefix[uri] = prefix } ns.uriToPrefix = uriToPrefix return nil } // EnsureResolved tries to resolve the given schema URI to a prefix and // returns the prefix and whether the resolution was successful. If the // URI hasn't been registered but a compatible version has, the // given URI is registered with the same prefix. func (ns *Namespace) EnsureResolved(uri string) (string, bool) { // TODO(rog) compatibility return ns.Resolve(uri) } // Resolve resolves the given schema URI to its registered prefix and // returns the prefix and whether the resolution was successful. // // If ns is nil, it is treated as if it were empty. // // Resolve does not mutate ns and may be called concurrently // with other non-mutating Namespace methods. func (ns *Namespace) Resolve(uri string) (string, bool) { if ns == nil { return "", false } prefix, ok := ns.uriToPrefix[uri] return prefix, ok } // ResolveCaveat resolves the given caveat by using // Resolve to map from its schema namespace to the appropriate prefix using // Resolve. If there is no registered prefix for the namespace, // it returns an error caveat. // // If ns.Namespace is empty or ns.Location is non-empty, it returns cav unchanged. // // If ns is nil, it is treated as if it were empty. // // ResolveCaveat does not mutate ns and may be called concurrently // with other non-mutating Namespace methods. func (ns *Namespace) ResolveCaveat(cav Caveat) Caveat { // TODO(rog) If a namespace isn't registered, try to resolve it by // resolving it to the latest compatible version that is // registered. if cav.Namespace == "" || cav.Location != "" { return cav } prefix, ok := ns.Resolve(cav.Namespace) if !ok { errCav := ErrorCaveatf("caveat %q in unregistered namespace %q", cav.Condition, cav.Namespace) if errCav.Namespace != cav.Namespace { prefix, _ = ns.Resolve(errCav.Namespace) } cav = errCav } if prefix != "" { cav.Condition = ConditionWithPrefix(prefix, cav.Condition) } cav.Namespace = "" return cav } // ConditionWithPrefix returns the given string prefixed by the // given prefix. If the prefix is non-empty, a colon // is used to separate them. func ConditionWithPrefix(prefix, condition string) string { if prefix == "" { return condition } return prefix + ":" + condition } // Register registers the given URI and associates it // with the given prefix. If the URI has already been registered, // this is a no-op. func (ns *Namespace) Register(uri, prefix string) { if !IsValidSchemaURI(uri) { panic(errgo.Newf("cannot register invalid URI %q (prefix %q)", uri, prefix)) } if !IsValidPrefix(prefix) { panic(errgo.Newf("cannot register invalid prefix %q for URI %q", prefix, uri)) } if _, ok := ns.uriToPrefix[uri]; !ok { ns.uriToPrefix[uri] = prefix } } func invalidSchemaRune(r rune) bool { return unicode.IsSpace(r) } // IsValidSchemaURI reports whether the given argument is suitable for // use as a namespace schema URI. It must be non-empty, a valid UTF-8 // string and it must not contain white space. func IsValidSchemaURI(uri string) bool { // TODO more stringent requirements? return len(uri) > 0 && utf8.ValidString(uri) && strings.IndexFunc(uri, invalidSchemaRune) == -1 } func invalidPrefixRune(r rune) bool { return r == ' ' || r == ':' || unicode.IsSpace(r) } func IsValidPrefix(prefix string) bool { return utf8.ValidString(prefix) && strings.IndexFunc(prefix, invalidPrefixRune) == -1 } macaroon-bakery-3.0.2/bakery/checkers/namespace_test.go000066400000000000000000000175401464427415500231770ustar00rootroot00000000000000package checkers_test import ( "testing" qt "github.com/frankban/quicktest" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/checkers" ) var resolveTests = []struct { about string ns *checkers.Namespace uri string expectPrefix string expectOK bool }{{ about: "successful resolve", ns: checkers.NewNamespace(map[string]string{"testns": "t"}), uri: "testns", expectPrefix: "t", expectOK: true, }, { about: "unsuccessful resolve", ns: checkers.NewNamespace(map[string]string{"testns": "t"}), uri: "foo", }, { about: "several of the same prefix", ns: checkers.NewNamespace(map[string]string{"testns": "t", "otherns": "t"}), uri: "otherns", expectPrefix: "t", expectOK: true, }, { about: "resolve with nil Namespace", uri: "testns", }} func TestResolve(t *testing.T) { c := qt.New(t) for i, test := range resolveTests { c.Logf("test %d: %s", i, test.about) prefix, ok := test.ns.Resolve(test.uri) c.Check(ok, qt.Equals, test.expectOK) c.Check(prefix, qt.Equals, test.expectPrefix) } } func TestRegister(t *testing.T) { c := qt.New(t) ns := checkers.NewNamespace(nil) ns.Register("testns", "t") prefix, ok := ns.Resolve("testns") c.Assert(prefix, qt.Equals, "t") c.Assert(ok, qt.Equals, true) ns.Register("other", "o") prefix, ok = ns.Resolve("other") c.Assert(prefix, qt.Equals, "o") c.Assert(ok, qt.Equals, true) // If we re-register the same URL, it does nothing. ns.Register("other", "p") prefix, ok = ns.Resolve("other") c.Assert(prefix, qt.Equals, "o") c.Assert(ok, qt.Equals, true) } var namespaceEqualTests = []struct { about string ns1, ns2 *checkers.Namespace expect bool }{{ about: "both nil", expect: true, }, { about: "ns1 nil", ns2: checkers.NewNamespace(nil), expect: false, }, { about: "ns2 nil", ns1: checkers.NewNamespace(nil), expect: false, }, { about: "different lengths", ns1: checkers.NewNamespace(map[string]string{"testns": "t", "otherns": "t"}), ns2: checkers.NewNamespace(map[string]string{"testns": "t"}), expect: false, }, { about: "all same", ns1: checkers.NewNamespace(map[string]string{"testns": "t", "otherns": "t"}), ns2: checkers.NewNamespace(map[string]string{"testns": "t", "otherns": "t"}), expect: true, }, { about: "different contents", ns1: checkers.NewNamespace(map[string]string{"testns": "t", "otherns": "t"}), ns2: checkers.NewNamespace(map[string]string{"testns": "t1", "otherns": "t"}), expect: false, }} func TestEqual(t *testing.T) { c := qt.New(t) for i, test := range namespaceEqualTests { c.Logf("test %d: %s", i, test.about) c.Assert(test.ns1.Equal(test.ns2), qt.Equals, test.expect) } } func TestRegisterBadURI(t *testing.T) { c := qt.New(t) ns := checkers.NewNamespace(nil) c.Assert(func() { ns.Register("", "x") }, qt.PanicMatches, `cannot register invalid URI "" \(prefix "x"\)`) } func TestRegisterBadPrefix(t *testing.T) { c := qt.New(t) ns := checkers.NewNamespace(nil) c.Assert(func() { ns.Register("std", "x:1") }, qt.PanicMatches, `cannot register invalid prefix "x:1" for URI "std"`) } var resolveCaveatTests = []struct { about string ns map[string]string caveat checkers.Caveat expect checkers.Caveat }{{ about: "no namespace", caveat: checkers.Caveat{ Condition: "foo", }, expect: checkers.Caveat{ Condition: "foo", }, }, { about: "with registered namespace", ns: map[string]string{ "testns": "t", }, caveat: checkers.Caveat{ Condition: "foo", Namespace: "testns", }, expect: checkers.Caveat{ Condition: "t:foo", }, }, { about: "with unregistered namespace", caveat: checkers.Caveat{ Condition: "foo", Namespace: "testns", }, expect: checkers.Caveat{ Condition: `error caveat "foo" in unregistered namespace "testns"`, }, }, { about: "with empty prefix", ns: map[string]string{ "testns": "", }, caveat: checkers.Caveat{ Condition: "foo", Namespace: "testns", }, expect: checkers.Caveat{ Condition: "foo", }, }} func TestResolveCaveatWithNamespace(t *testing.T) { c := qt.New(t) for i, test := range resolveCaveatTests { c.Logf("test %d: %s", i, test.about) ns := checkers.NewNamespace(test.ns) c.Assert(ns.ResolveCaveat(test.caveat), qt.DeepEquals, test.expect) } } var namespaceMarshalTests = []struct { about string ns map[string]string expect string }{{ about: "empty namespace", }, { about: "standard namespace", ns: map[string]string{ "std": "", }, expect: "std:", }, { about: "several elements", ns: map[string]string{ "std": "", "http://blah.blah": "blah", "one": "two", "foo.com/x.v0.1": "z", }, expect: "foo.com/x.v0.1:z http://blah.blah:blah one:two std:", }, { about: "sort by URI not by field", ns: map[string]string{ "a": "one", "a1": "two", // Note that '1' < ':' }, expect: "a:one a1:two", }} func TestMarshal(t *testing.T) { c := qt.New(t) for i, test := range namespaceMarshalTests { c.Logf("test %d: %v", i, test.about) ns := checkers.NewNamespace(test.ns) data, err := ns.MarshalText() c.Assert(err, qt.Equals, nil) c.Assert(string(data), qt.Equals, test.expect) c.Assert(ns.String(), qt.Equals, test.expect) // Check that it can be unmarshaled to the same thing: var ns1 checkers.Namespace err = ns1.UnmarshalText(data) c.Assert(err, qt.Equals, nil) c.Assert(&ns1, qt.DeepEquals, ns) } } var namespaceUnmarshalTests = []struct { about string text string expect map[string]string expectError string }{{ about: "empty text", }, { about: "fields with extra space", text: " x:y \t\nz:\r", expect: map[string]string{ "x": "y", "z": "", }, }, { about: "field without colon", text: "foo:x bar baz:g", expectError: `no colon in namespace field "bar"`, }, { about: "invalid URI", text: "foo\xff:a", expectError: `invalid URI "foo\\xff" in namespace field "foo\\xff:a"`, }, { about: "empty URI", text: "blah:x :b", expectError: `invalid URI "" in namespace field ":b"`, }, { about: "invalid prefix", text: "p:\xff", expectError: `invalid prefix "\\xff" in namespace field "p:\\xff"`, }, { about: "duplicate URI", text: "std: std:p", expectError: `duplicate URI "std" in namespace "std: std:p"`, }} func TestUnmarshal(t *testing.T) { c := qt.New(t) for i, test := range namespaceUnmarshalTests { c.Logf("test %d: %v", i, test.about) var ns checkers.Namespace err := ns.UnmarshalText([]byte(test.text)) if test.expectError != "" { c.Assert(err, qt.ErrorMatches, test.expectError) } else { c.Assert(err, qt.Equals, nil) c.Assert(&ns, qt.DeepEquals, checkers.NewNamespace(test.expect)) } } } func TestMarshalNil(t *testing.T) { c := qt.New(t) var ns *checkers.Namespace data, err := ns.MarshalText() c.Assert(err, qt.Equals, nil) c.Assert(data, qt.HasLen, 0) } var validTests = []struct { about string test func(string) bool s string expect bool }{{ about: "URI with schema", test: checkers.IsValidSchemaURI, s: "http://foo.com", expect: true, }, { about: "URI with space", test: checkers.IsValidSchemaURI, s: "a\rb", }, { about: "URI with unicode space", test: checkers.IsValidSchemaURI, s: "x\u2003y", }, { about: "empty URI", test: checkers.IsValidSchemaURI, }, { about: "URI with invalid UTF-8", test: checkers.IsValidSchemaURI, s: "\xff", }, { about: "prefix with colon", test: checkers.IsValidPrefix, s: "x:y", }, { about: "prefix with space", test: checkers.IsValidPrefix, s: "x y", }, { about: "prefix with unicode space", test: checkers.IsValidPrefix, s: "\u3000", }, { about: "empty prefix", test: checkers.IsValidPrefix, expect: true, }} func TestValid(t *testing.T) { c := qt.New(t) for i, test := range validTests { c.Check(test.test(test.s), qt.Equals, test.expect, qt.Commentf("test %d: %s", i, test.about)) } } macaroon-bakery-3.0.2/bakery/checkers/time.go000066400000000000000000000044311464427415500211350ustar00rootroot00000000000000package checkers import ( "context" "fmt" "time" "gopkg.in/errgo.v1" "gopkg.in/macaroon.v2" ) // Clock represents a clock that can be faked for testing purposes. type Clock interface { Now() time.Time } type timeKey struct{} func ContextWithClock(ctx context.Context, clock Clock) context.Context { if clock == nil { return ctx } return context.WithValue(ctx, timeKey{}, clock) } func clockFromContext(ctx context.Context) Clock { c, _ := ctx.Value(timeKey{}).(Clock) return c } func checkTimeBefore(ctx context.Context, _, arg string) error { var now time.Time if clock := clockFromContext(ctx); clock != nil { now = clock.Now() } else { now = time.Now() } t, err := time.Parse(time.RFC3339Nano, arg) if err != nil { return errgo.Mask(err) } if !now.Before(t) { return fmt.Errorf("macaroon has expired") } return nil } // TimeBeforeCaveat returns a caveat that specifies that // the time that it is checked should be before t. func TimeBeforeCaveat(t time.Time) Caveat { return firstParty(CondTimeBefore, t.UTC().Format(time.RFC3339Nano)) } // ExpiryTime returns the minimum time of any time-before caveats found // in the given slice and whether there were any such caveats found. // // The ns parameter is used to determine the standard namespace prefix - if // the standard namespace is not found, the empty prefix is assumed. func ExpiryTime(ns *Namespace, cavs []macaroon.Caveat) (time.Time, bool) { prefix, _ := ns.Resolve(StdNamespace) timeBeforeCond := ConditionWithPrefix(prefix, CondTimeBefore) var t time.Time var expires bool for _, cav := range cavs { cav := string(cav.Id) name, rest, _ := ParseCaveat(cav) if name != timeBeforeCond { continue } et, err := time.Parse(time.RFC3339Nano, rest) if err != nil { continue } if !expires || et.Before(t) { t = et expires = true } } return t, expires } // MacaroonsExpiryTime returns the minimum time of any time-before // caveats found in the given macaroons and whether there were // any such caveats found. func MacaroonsExpiryTime(ns *Namespace, ms macaroon.Slice) (time.Time, bool) { var t time.Time var expires bool for _, m := range ms { if et, ex := ExpiryTime(ns, m.Caveats()); ex { if !expires || et.Before(t) { t = et expires = true } } } return t, expires } macaroon-bakery-3.0.2/bakery/checkers/time_test.go000066400000000000000000000073121464427415500221750ustar00rootroot00000000000000package checkers_test import ( "testing" "time" qt "github.com/frankban/quicktest" "gopkg.in/macaroon.v2" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/checkers" ) var t1 = time.Now() var t2 = t1.Add(1 * time.Hour) var t3 = t2.Add(1 * time.Hour) var expireTimeTests = []struct { about string caveats []macaroon.Caveat expectTime time.Time expectExpires bool }{{ about: "nil caveats", }, { about: "empty caveats", caveats: []macaroon.Caveat{}, }, { about: "single time-before caveat", caveats: []macaroon.Caveat{ macaroon.Caveat{ Id: []byte(checkers.TimeBeforeCaveat(t1).Condition), }, }, expectTime: t1, expectExpires: true, }, { about: "multiple time-before caveat", caveats: []macaroon.Caveat{ macaroon.Caveat{ Id: []byte(checkers.TimeBeforeCaveat(t2).Condition), }, macaroon.Caveat{ Id: []byte(checkers.TimeBeforeCaveat(t1).Condition), }, }, expectTime: t1, expectExpires: true, }, { about: "mixed caveats", caveats: []macaroon.Caveat{ macaroon.Caveat{ Id: []byte(checkers.TimeBeforeCaveat(t1).Condition), }, macaroon.Caveat{ Id: []byte("allow bar"), }, macaroon.Caveat{ Id: []byte(checkers.TimeBeforeCaveat(t2).Condition), }, macaroon.Caveat{ Id: []byte("deny foo"), }, }, expectTime: t1, expectExpires: true, }, { about: "invalid time-before caveat", caveats: []macaroon.Caveat{ macaroon.Caveat{ Id: []byte(checkers.CondTimeBefore + " tomorrow"), }, }, }} func TestExpireTime(t *testing.T) { c := qt.New(t) for i, test := range expireTimeTests { c.Logf("%d. %s", i, test.about) t, expires := checkers.ExpiryTime(nil, test.caveats) c.Assert(t.Equal(test.expectTime), qt.Equals, true, qt.Commentf("obtained: %s, expected: %s", t, test.expectTime)) c.Assert(expires, qt.Equals, test.expectExpires) } } var macaroonsExpireTimeTests = []struct { about string macaroons macaroon.Slice expectTime time.Time expectExpires bool }{{ about: "nil macaroons", }, { about: "empty macaroons", macaroons: macaroon.Slice{}, }, { about: "single macaroon without caveats", macaroons: macaroon.Slice{ mustNewMacaroon(), }, }, { about: "multiple macaroon without caveats", macaroons: macaroon.Slice{ mustNewMacaroon(), mustNewMacaroon(), }, }, { about: "single macaroon with time-before caveat", macaroons: macaroon.Slice{ mustNewMacaroon( checkers.TimeBeforeCaveat(t1).Condition, ), }, expectTime: t1, expectExpires: true, }, { about: "single macaroon with multiple time-before caveats", macaroons: macaroon.Slice{ mustNewMacaroon( checkers.TimeBeforeCaveat(t2).Condition, checkers.TimeBeforeCaveat(t1).Condition, ), }, expectTime: t1, expectExpires: true, }, { about: "multiple macaroons with multiple time-before caveats", macaroons: macaroon.Slice{ mustNewMacaroon( checkers.TimeBeforeCaveat(t3).Condition, checkers.TimeBeforeCaveat(t2).Condition, ), mustNewMacaroon( checkers.TimeBeforeCaveat(t3).Condition, checkers.TimeBeforeCaveat(t1).Condition, ), }, expectTime: t1, expectExpires: true, }} func TestMacaroonsExpireTime(t *testing.T) { c := qt.New(t) for i, test := range macaroonsExpireTimeTests { c.Logf("%d. %s", i, test.about) t, expires := checkers.MacaroonsExpiryTime(nil, test.macaroons) c.Assert(t.Equal(test.expectTime), qt.Equals, true, qt.Commentf("obtained: %s, expected: %s", t, test.expectTime)) c.Assert(expires, qt.Equals, test.expectExpires) } } func mustNewMacaroon(cavs ...string) *macaroon.Macaroon { m, err := macaroon.New(nil, nil, "", macaroon.LatestVersion) if err != nil { panic(err) } for _, cav := range cavs { if err := m.AddFirstPartyCaveat([]byte(cav)); err != nil { panic(err) } } return m } macaroon-bakery-3.0.2/bakery/codec.go000066400000000000000000000273251464427415500174740ustar00rootroot00000000000000package bakery import ( "bytes" "crypto/rand" "encoding/base64" "encoding/binary" "encoding/json" "golang.org/x/crypto/nacl/box" "gopkg.in/errgo.v1" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/checkers" ) type caveatRecord struct { RootKey []byte Condition string } // caveatJSON defines the format of a V1 JSON-encoded third party caveat id. type caveatJSON struct { ThirdPartyPublicKey *PublicKey FirstPartyPublicKey *PublicKey Nonce []byte Id string } // encodeCaveat encrypts a third-party caveat with the given condtion // and root key. The thirdPartyInfo key holds information about the // third party we're encrypting the caveat for; the key is the // public/private key pair of the party that's adding the caveat. // // The caveat will be encoded according to the version information // found in thirdPartyInfo. func encodeCaveat( condition string, rootKey []byte, thirdPartyInfo ThirdPartyInfo, key *KeyPair, ns *checkers.Namespace, ) ([]byte, error) { switch thirdPartyInfo.Version { case Version0, Version1: return encodeCaveatV1(condition, rootKey, &thirdPartyInfo.PublicKey, key) case Version2: return encodeCaveatV2(condition, rootKey, &thirdPartyInfo.PublicKey, key) default: // Version 3 or later - use V3. return encodeCaveatV3(condition, rootKey, &thirdPartyInfo.PublicKey, key, ns) } } // encodeCaveatV1 creates a JSON-encoded third-party caveat // with the given condtion and root key. The thirdPartyPubKey key // represents the public key of the third party we're encrypting // the caveat for; the key is the public/private key pair of the party // that's adding the caveat. func encodeCaveatV1( condition string, rootKey []byte, thirdPartyPubKey *PublicKey, key *KeyPair, ) ([]byte, error) { var nonce [NonceLen]byte if _, err := rand.Read(nonce[:]); err != nil { return nil, errgo.Notef(err, "cannot generate random number for nonce") } plain := caveatRecord{ RootKey: rootKey, Condition: condition, } plainData, err := json.Marshal(&plain) if err != nil { return nil, errgo.Notef(err, "cannot marshal %#v", &plain) } sealed := box.Seal(nil, plainData, &nonce, thirdPartyPubKey.boxKey(), key.Private.boxKey()) id := caveatJSON{ ThirdPartyPublicKey: thirdPartyPubKey, FirstPartyPublicKey: &key.Public, Nonce: nonce[:], Id: base64.StdEncoding.EncodeToString(sealed), } data, err := json.Marshal(id) if err != nil { return nil, errgo.Notef(err, "cannot marshal %#v", id) } buf := make([]byte, base64.StdEncoding.EncodedLen(len(data))) base64.StdEncoding.Encode(buf, data) return buf, nil } // encodeCaveatV2 creates a version 2 third-party caveat. func encodeCaveatV2( condition string, rootKey []byte, thirdPartyPubKey *PublicKey, key *KeyPair, ) ([]byte, error) { return encodeCaveatV2V3(Version2, condition, rootKey, thirdPartyPubKey, key, nil) } // encodeCaveatV3 creates a version 3 third-party caveat. func encodeCaveatV3( condition string, rootKey []byte, thirdPartyPubKey *PublicKey, key *KeyPair, ns *checkers.Namespace, ) ([]byte, error) { return encodeCaveatV2V3(Version3, condition, rootKey, thirdPartyPubKey, key, ns) } const publicKeyPrefixLen = 4 // version3CaveatMinLen holds an underestimate of the // minimum length of a version 3 caveat. const version3CaveatMinLen = 1 + 4 + 32 + 24 + box.Overhead + 1 // encodeCaveatV3 creates a version 2 or version 3 third-party caveat. // // The format has the following packed binary fields (note // that all fields up to and including the nonce are the same // as the v2 format): // // version 2 or 3 [1 byte] // first 4 bytes of third-party Curve25519 public key [4 bytes] // first-party Curve25519 public key [32 bytes] // nonce [24 bytes] // encrypted secret part [rest of message] // // The encrypted part encrypts the following fields // with box.Seal: // // version 2 or 3 [1 byte] // length of root key [n: uvarint] // root key [n bytes] // length of encoded namespace [n: uvarint] (Version 3 only) // encoded namespace [n bytes] (Version 3 only) // condition [rest of encrypted part] func encodeCaveatV2V3( version Version, condition string, rootKey []byte, thirdPartyPubKey *PublicKey, key *KeyPair, ns *checkers.Namespace, ) ([]byte, error) { var nsData []byte if version >= Version3 { data, err := ns.MarshalText() if err != nil { return nil, errgo.Mask(err) } nsData = data } // dataLen is our estimate of how long the data will be. // As we always use append, this doesn't have to be strictly // accurate but it's nice to avoid allocations. dataLen := 0 + 1 + // version publicKeyPrefixLen + KeyLen + NonceLen + box.Overhead + 1 + // version uvarintLen(uint64(len(rootKey))) + len(rootKey) + uvarintLen(uint64(len(nsData))) + len(nsData) + len(condition) var nonce [NonceLen]byte = uuidGen.Next() data := make([]byte, 0, dataLen) data = append(data, byte(version)) data = append(data, thirdPartyPubKey.Key[:publicKeyPrefixLen]...) data = append(data, key.Public.Key[:]...) data = append(data, nonce[:]...) secret := encodeSecretPartV2V3(version, condition, rootKey, nsData) return box.Seal(data, secret, &nonce, thirdPartyPubKey.boxKey(), key.Private.boxKey()), nil } // encodeSecretPartV2V3 creates a version 2 or version 3 secret part of the third party // caveat. The returned data is not encrypted. // // The format has the following packed binary fields: // version 2 or 3 [1 byte] // root key length [n: uvarint] // root key [n bytes] // namespace length [n: uvarint] (v3 only) // namespace [n bytes] (v3 only) // predicate [rest of message] func encodeSecretPartV2V3(version Version, condition string, rootKey, nsData []byte) []byte { data := make([]byte, 0, 1+binary.MaxVarintLen64+len(rootKey)+len(condition)) data = append(data, byte(version)) // version data = appendUvarint(data, uint64(len(rootKey))) data = append(data, rootKey...) if version >= Version3 { data = appendUvarint(data, uint64(len(nsData))) data = append(data, nsData...) } data = append(data, condition...) return data } // decodeCaveat attempts to decode caveat by decrypting the encrypted part // using key. func decodeCaveat(key *KeyPair, caveat []byte) (*ThirdPartyCaveatInfo, error) { if len(caveat) == 0 { return nil, errgo.New("empty third party caveat") } switch caveat[0] { case byte(Version2): return decodeCaveatV2V3(Version2, key, caveat) case byte(Version3): if len(caveat) < version3CaveatMinLen { // If it has the version 3 caveat tag and it's too short, it's // almost certainly an id, not an encrypted payload. return nil, errgo.Newf("caveat id payload not provided for caveat id %q", caveat) } return decodeCaveatV2V3(Version3, key, caveat) case 'e': // 'e' will be the first byte if the caveatid is a base64 encoded JSON object. return decodeCaveatV1(key, caveat) default: return nil, errgo.Newf("caveat has unsupported version %d", caveat[0]) } } // decodeCaveatV1 attempts to decode a base64 encoded JSON id. This // encoding is nominally version -1. func decodeCaveatV1(key *KeyPair, caveat []byte) (*ThirdPartyCaveatInfo, error) { data := make([]byte, (3*len(caveat)+3)/4) n, err := base64.StdEncoding.Decode(data, caveat) if err != nil { return nil, errgo.Notef(err, "cannot base64-decode caveat") } data = data[:n] var wrapper caveatJSON if err := json.Unmarshal(data, &wrapper); err != nil { return nil, errgo.Notef(err, "cannot unmarshal caveat %q", data) } if !bytes.Equal(key.Public.Key[:], wrapper.ThirdPartyPublicKey.Key[:]) { return nil, errgo.New("public key mismatch") } if wrapper.FirstPartyPublicKey == nil { return nil, errgo.New("target service public key not specified") } // The encrypted string is base64 encoded in the JSON representation. secret, err := base64.StdEncoding.DecodeString(wrapper.Id) if err != nil { return nil, errgo.Notef(err, "cannot base64-decode encrypted data") } var nonce [NonceLen]byte if copy(nonce[:], wrapper.Nonce) < NonceLen { return nil, errgo.Newf("nonce too short %x", wrapper.Nonce) } c, ok := box.Open(nil, secret, &nonce, wrapper.FirstPartyPublicKey.boxKey(), key.Private.boxKey()) if !ok { return nil, errgo.Newf("cannot decrypt caveat %#v", wrapper) } var record caveatRecord if err := json.Unmarshal(c, &record); err != nil { return nil, errgo.Notef(err, "cannot decode third party caveat record") } return &ThirdPartyCaveatInfo{ Condition: []byte(record.Condition), FirstPartyPublicKey: *wrapper.FirstPartyPublicKey, ThirdPartyKeyPair: *key, RootKey: record.RootKey, Caveat: caveat, Version: Version1, Namespace: legacyNamespace(), }, nil } // decodeCaveatV2V3 decodes a version 2 or version 3 caveat. func decodeCaveatV2V3(version Version, key *KeyPair, caveat []byte) (*ThirdPartyCaveatInfo, error) { origCaveat := caveat if len(caveat) < 1+publicKeyPrefixLen+KeyLen+NonceLen+box.Overhead { return nil, errgo.New("caveat id too short") } caveat = caveat[1:] // skip version (already checked) publicKeyPrefix, caveat := caveat[:publicKeyPrefixLen], caveat[publicKeyPrefixLen:] if !bytes.Equal(key.Public.Key[:publicKeyPrefixLen], publicKeyPrefix) { return nil, errgo.New("public key mismatch") } var firstPartyPub PublicKey copy(firstPartyPub.Key[:], caveat[:KeyLen]) caveat = caveat[KeyLen:] var nonce [NonceLen]byte copy(nonce[:], caveat[:NonceLen]) caveat = caveat[NonceLen:] data, ok := box.Open(nil, caveat, &nonce, firstPartyPub.boxKey(), key.Private.boxKey()) if !ok { return nil, errgo.Newf("cannot decrypt caveat id") } rootKey, ns, condition, err := decodeSecretPartV2V3(version, data) if err != nil { return nil, errgo.Notef(err, "invalid secret part") } return &ThirdPartyCaveatInfo{ Condition: condition, FirstPartyPublicKey: firstPartyPub, ThirdPartyKeyPair: *key, RootKey: rootKey, Caveat: origCaveat, Version: version, Namespace: ns, }, nil } func decodeSecretPartV2V3(version Version, data []byte) (rootKey []byte, ns *checkers.Namespace, condition []byte, err error) { fail := func(err error) ([]byte, *checkers.Namespace, []byte, error) { return nil, nil, nil, err } if len(data) < 1 { return fail(errgo.New("secret part too short")) } gotVersion, data := data[0], data[1:] if version != Version(gotVersion) { return fail(errgo.Newf("unexpected secret part version, got %d want %d", gotVersion, version)) } l, n := binary.Uvarint(data) if n <= 0 || uint64(n)+l > uint64(len(data)) { return fail(errgo.Newf("invalid root key length")) } data = data[n:] rootKey, data = data[:l], data[l:] if version >= Version3 { var nsData []byte var ns1 checkers.Namespace l, n = binary.Uvarint(data) if n <= 0 || uint64(n)+l > uint64(len(data)) { return fail(errgo.Newf("invalid namespace length")) } data = data[n:] nsData, data = data[:l], data[l:] if err := ns1.UnmarshalText(nsData); err != nil { return fail(errgo.Notef(err, "cannot unmarshal namespace")) } ns = &ns1 } else { ns = legacyNamespace() } return rootKey, ns, data, nil } // appendUvarint appends n to data encoded as a variable-length // unsigned integer. func appendUvarint(data []byte, n uint64) []byte { // Ensure the capacity is sufficient. If our space calculations when // allocating data were correct, this should never happen, // but be defensive just in case. for need := uvarintLen(n); cap(data)-len(data) < need; { data1 := append(data[0:cap(data)], 0) data = data1[0:len(data)] } nlen := binary.PutUvarint(data[len(data):cap(data)], n) return data[0 : len(data)+nlen] } // uvarintLen returns the number of bytes that n will require // when encoded with binary.PutUvarint. func uvarintLen(n uint64) int { len := 1 n >>= 7 for ; n > 0; n >>= 7 { len++ } return len } macaroon-bakery-3.0.2/bakery/codec_test.go000066400000000000000000000132611464427415500205250ustar00rootroot00000000000000package bakery import ( "bytes" "testing" qt "github.com/frankban/quicktest" "golang.org/x/crypto/nacl/box" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/checkers" ) var ( testFirstPartyKey = MustGenerateKey() testThirdPartyKey = MustGenerateKey() ) func TestV1RoundTrip(t *testing.T) { c := qt.New(t) cid, err := encodeCaveatV1( "is-authenticated-user", []byte("a random string"), &testThirdPartyKey.Public, testFirstPartyKey) c.Assert(err, qt.IsNil) res, err := decodeCaveat(testThirdPartyKey, cid) c.Assert(err, qt.IsNil) c.Assert(res, qt.DeepEquals, &ThirdPartyCaveatInfo{ FirstPartyPublicKey: testFirstPartyKey.Public, RootKey: []byte("a random string"), Condition: []byte("is-authenticated-user"), Caveat: cid, ThirdPartyKeyPair: *testThirdPartyKey, Version: Version1, Namespace: legacyNamespace(), }) } func TestV2RoundTrip(t *testing.T) { c := qt.New(t) cid, err := encodeCaveatV2("is-authenticated-user", []byte("a random string"), &testThirdPartyKey.Public, testFirstPartyKey) c.Assert(err, qt.IsNil) res, err := decodeCaveat(testThirdPartyKey, cid) c.Assert(err, qt.IsNil) c.Assert(res, qt.DeepEquals, &ThirdPartyCaveatInfo{ FirstPartyPublicKey: testFirstPartyKey.Public, RootKey: []byte("a random string"), Condition: []byte("is-authenticated-user"), Caveat: cid, ThirdPartyKeyPair: *testThirdPartyKey, Version: Version2, Namespace: legacyNamespace(), }) } func TestV3RoundTrip(t *testing.T) { c := qt.New(t) ns := checkers.NewNamespace(nil) ns.Register("testns", "x") cid, err := encodeCaveatV3("is-authenticated-user", []byte("a random string"), &testThirdPartyKey.Public, testFirstPartyKey, ns) c.Assert(err, qt.IsNil) c.Logf("cid %x", cid) res, err := decodeCaveat(testThirdPartyKey, cid) c.Assert(err, qt.IsNil) c.Assert(res, qt.DeepEquals, &ThirdPartyCaveatInfo{ FirstPartyPublicKey: testFirstPartyKey.Public, RootKey: []byte("a random string"), Condition: []byte("is-authenticated-user"), Caveat: cid, ThirdPartyKeyPair: *testThirdPartyKey, Version: Version3, Namespace: ns, }) } func TestEmptyCaveatId(t *testing.T) { c := qt.New(t) _, err := decodeCaveat(testThirdPartyKey, []byte{}) c.Assert(err, qt.ErrorMatches, "empty third party caveat") } func TestCaveatIdBadVersion(t *testing.T) { c := qt.New(t) _, err := decodeCaveat(testThirdPartyKey, []byte{1}) c.Assert(err, qt.ErrorMatches, "caveat has unsupported version 1") } func TestV2TooShort(t *testing.T) { c := qt.New(t) _, err := decodeCaveat(testThirdPartyKey, []byte{2}) c.Assert(err, qt.ErrorMatches, "caveat id too short") } func TestV2BadKey(t *testing.T) { c := qt.New(t) cid, err := encodeCaveatV2("is-authenticated-user", []byte("a random string"), &testThirdPartyKey.Public, testFirstPartyKey) c.Assert(err, qt.IsNil) cid[1] ^= 1 _, err = decodeCaveat(testThirdPartyKey, cid) c.Assert(err, qt.ErrorMatches, "public key mismatch") } func TestV2DecryptionError(t *testing.T) { c := qt.New(t) cid, err := encodeCaveatV2("is-authenticated-user", []byte("a random string"), &testThirdPartyKey.Public, testFirstPartyKey) c.Assert(err, qt.IsNil) cid[5] ^= 1 _, err = decodeCaveat(testThirdPartyKey, cid) c.Assert(err, qt.ErrorMatches, "cannot decrypt caveat id") } func TestV2EmptySecretPart(t *testing.T) { c := qt.New(t) cid, err := encodeCaveatV2("is-authenticated-user", []byte("a random string"), &testThirdPartyKey.Public, testFirstPartyKey) c.Assert(err, qt.IsNil) cid = replaceV2SecretPart(cid, []byte{}) _, err = decodeCaveat(testThirdPartyKey, cid) c.Assert(err, qt.ErrorMatches, "invalid secret part: secret part too short") } func TestV2BadSecretPartVersion(t *testing.T) { c := qt.New(t) cid, err := encodeCaveatV2("is-authenticated-user", []byte("a random string"), &testThirdPartyKey.Public, testFirstPartyKey) c.Assert(err, qt.IsNil) cid = replaceV2SecretPart(cid, []byte{1}) _, err = decodeCaveat(testThirdPartyKey, cid) c.Assert(err, qt.ErrorMatches, "invalid secret part: unexpected secret part version, got 1 want 2") } func TestV2EmptyRootKey(t *testing.T) { c := qt.New(t) cid, err := encodeCaveatV2("is-authenticated-user", []byte{}, &testThirdPartyKey.Public, testFirstPartyKey) c.Assert(err, qt.IsNil) res, err := decodeCaveat(testThirdPartyKey, cid) c.Assert(err, qt.IsNil) c.Assert(res, qt.DeepEquals, &ThirdPartyCaveatInfo{ FirstPartyPublicKey: testFirstPartyKey.Public, RootKey: []byte{}, Condition: []byte("is-authenticated-user"), Caveat: cid, ThirdPartyKeyPair: *testThirdPartyKey, Version: Version2, Namespace: legacyNamespace(), }) } func TestV2LongRootKey(t *testing.T) { c := qt.New(t) cid, err := encodeCaveatV2("is-authenticated-user", bytes.Repeat([]byte{0}, 65536), &testThirdPartyKey.Public, testFirstPartyKey) c.Assert(err, qt.IsNil) res, err := decodeCaveat(testThirdPartyKey, cid) c.Assert(err, qt.IsNil) c.Assert(res, qt.DeepEquals, &ThirdPartyCaveatInfo{ FirstPartyPublicKey: testFirstPartyKey.Public, RootKey: bytes.Repeat([]byte{0}, 65536), Condition: []byte("is-authenticated-user"), Caveat: cid, ThirdPartyKeyPair: *testThirdPartyKey, Version: Version2, Namespace: legacyNamespace(), }) } func replaceV2SecretPart(cid, replacement []byte) []byte { cid = cid[:1+publicKeyPrefixLen+KeyLen+NonceLen] var nonce [NonceLen]byte copy(nonce[:], cid[1+publicKeyPrefixLen+KeyLen:]) return box.Seal(cid, replacement, &nonce, testFirstPartyKey.Public.boxKey(), testThirdPartyKey.Private.boxKey()) } macaroon-bakery-3.0.2/bakery/common_test.go000066400000000000000000000063261464427415500207440ustar00rootroot00000000000000package bakery_test import ( "context" "encoding/json" "fmt" "time" qt "github.com/frankban/quicktest" "gopkg.in/macaroon.v2" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/checkers" ) // testContext holds the testing background context - its associated time when checking // time-before caveats will always be the value of epoch. var testContext = checkers.ContextWithClock(context.Background(), stoppedClock{epoch}) var basicOp = bakery.Op{"basic", "basic"} var ( epoch = time.Date(1900, 11, 17, 19, 00, 13, 0, time.UTC) ) var testChecker = func() *checkers.Checker { c := checkers.New(nil) c.Namespace().Register("testns", "") c.Register("str", "testns", strCheck) c.Register("true", "testns", trueCheck) return c }() // newBakery returns a new Bakery instance using a new // key pair, and registers the key with the given locator if provided. // // It uses testChecker to check first party caveats. func newBakery(location string, locator *bakery.ThirdPartyStore) *bakery.Bakery { key := mustGenerateKey() p := bakery.BakeryParams{ Key: key, Checker: testChecker, Location: location, } if locator != nil { p.Locator = locator locator.AddInfo(location, bakery.ThirdPartyInfo{ PublicKey: key.Public, Version: bakery.LatestVersion, }) } return bakery.New(p) } func noDischarge(c *qt.C) func(context.Context, macaroon.Caveat, []byte) (*bakery.Macaroon, error) { return func(context.Context, macaroon.Caveat, []byte) (*bakery.Macaroon, error) { c.Errorf("getDischarge called unexpectedly") return nil, fmt.Errorf("nothing") } } type strKey struct{} func strContext(s string) context.Context { return context.WithValue(testContext, strKey{}, s) } func strCaveat(s string) checkers.Caveat { return checkers.Caveat{ Condition: "str " + s, Namespace: "testns", } } func trueCaveat(s string) checkers.Caveat { return checkers.Caveat{ Condition: "true " + s, Namespace: "testns", } } // trueCheck always succeeds. func trueCheck(ctx context.Context, cond, args string) error { return nil } // strCheck checks that the string value in the context // matches the argument to the condition. func strCheck(ctx context.Context, cond, args string) error { expect, _ := ctx.Value(strKey{}).(string) if args != expect { return fmt.Errorf("%s doesn't match %s", cond, expect) } return nil } type thirdPartyStrcmpChecker string func (c thirdPartyStrcmpChecker) CheckThirdPartyCaveat(_ context.Context, cavInfo *bakery.ThirdPartyCaveatInfo) ([]checkers.Caveat, error) { if string(cavInfo.Condition) != string(c) { return nil, fmt.Errorf("%s doesn't match %s", cavInfo.Condition, c) } return nil, nil } type thirdPartyCheckerWithCaveats []checkers.Caveat func (c thirdPartyCheckerWithCaveats) CheckThirdPartyCaveat(_ context.Context, cavInfo *bakery.ThirdPartyCaveatInfo) ([]checkers.Caveat, error) { return c, nil } func macStr(m *macaroon.Macaroon) string { data, err := json.MarshalIndent(m, "\t", "\t") if err != nil { panic(err) } return string(data) } type stoppedClock struct { t time.Time } func (t stoppedClock) Now() time.Time { return t.t } func mustGenerateKey() *bakery.KeyPair { return bakery.MustGenerateKey() } macaroon-bakery-3.0.2/bakery/dbrootkeystore/000077500000000000000000000000001464427415500211365ustar00rootroot00000000000000macaroon-bakery-3.0.2/bakery/dbrootkeystore/rootkey.go000066400000000000000000000313421464427415500231640ustar00rootroot00000000000000// Package dbkeystore provides the underlying basis for a bakery.RootKeyStore // that uses a database as a persistent store and provides flexible policies // for root key storage lifetime. package dbrootkeystore import ( "context" "crypto/rand" "fmt" "sync" "time" "gopkg.in/errgo.v1" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" ) // maxPolicyCache holds the maximum number of store policies that can // hold cached keys in a given RootKeys instance. // // 100 is probably overkill, given that practical systems will // likely only have a small number of active policies on any given // macaroon collection. const maxPolicyCache = 100 // RootKeys represents a cache of macaroon root keys. type RootKeys struct { maxCacheSize int clock Clock // TODO (rogpeppe) use RWMutex instead of Mutex here so that // it's faster in the probably-common case that we // have many contended readers. mu sync.Mutex oldCache map[string]RootKey cache map[string]RootKey // current holds the current root key for each store policy. current map[Policy]RootKey } // Clock can be used to provide a mockable time // of day for testing. type Clock interface { Now() time.Time } // Backing holds the interface used to store keys in the underlying // database used as a backing store by RootKeyStore. type Backing interface { // GetKey gets the key with the given id from the // backing store. If the key is not found, it should // return an error with a bakery.ErrNotFound cause. GetKey(id []byte) (RootKey, error) // FindLatestKey returns the most recently created root key k // such that all of the following conditions hold: // // k.Created >= createdAfter // k.Expires >= expiresAfter // k.Expires <= expiresBefore // // If no such key was found, the zero root key should be returned // with a nil error. FindLatestKey(createdAfter, expiresAfter, expiresBefore time.Time) (RootKey, error) // InsertKey inserts the given root key into the backing store. // It may return an error if the id or key already exist. InsertKey(key RootKey) error } // A ContextBacking is like a Backing but all methods accept a // context.Context. If a Backing also implements ContextBacking then the // ContextBacking methods will be used in preference. type ContextBacking interface { // GetKeyContext gets the key with the given id from the backing // store. If the key is not found, it should return an error with // a bakery.ErrNotFound cause. GetKeyContext(ctx context.Context, id []byte) (RootKey, error) // FindLatestKeyContext returns the most recently created root // key k such that all of the following conditions hold: // // k.Created >= createdAfter // k.Expires >= expiresAfter // k.Expires <= expiresBefore // // If no such key was found, the zero root key should be returned // with a nil error. FindLatestKeyContext(ctx context.Context, createdAfter, expiresAfter, expiresBefore time.Time) (RootKey, error) // InsertKeyContext inserts the given root key into the backing // store. It may return an error if the id or key already exist. InsertKeyContext(ctx context.Context, key RootKey) error } // A backingWrapper is used to convert a Backing into a ContextBacking by // accepting and ignoring the contexts. type backingWrapper struct { b Backing } // GetKey implements ContextBacking. func (w backingWrapper) GetKeyContext(_ context.Context, id []byte) (RootKey, error) { return w.b.GetKey(id) } // FindLatestKey implements ContextBacking. func (w backingWrapper) FindLatestKeyContext(_ context.Context, createdAfter, expiresAfter, expiresBefore time.Time) (RootKey, error) { return w.b.FindLatestKey(createdAfter, expiresAfter, expiresBefore) } // InsertKey implements ContextBacking. func (w backingWrapper) InsertKeyContext(_ context.Context, key RootKey) error { return w.b.InsertKey(key) } // RootKey is the type stored in the underlying database. type RootKey struct { // Id holds the id of the root key. Id []byte `bson:"_id"` // Created holds the time that the root key was created. Created time.Time // Expires holds the time that the root key expires. Expires time.Time // RootKey holds the root key secret itself. RootKey []byte } // IsValid reports whether the root key contains a key. Note that we // always generate non-empty root keys, so we use this to find // whether the root key is empty or not. func (rk RootKey) IsValid() bool { return rk.RootKey != nil } // IsValidWithPolicy reports whether the given root key // is valid to use at the given time with the given store policy. func (rk RootKey) IsValidWithPolicy(p Policy, now time.Time) bool { if !rk.IsValid() { return false } return afterEq(rk.Created, now.Add(-p.GenerateInterval)) && afterEq(rk.Expires, now.Add(p.ExpiryDuration)) && beforeEq(rk.Expires, now.Add(p.ExpiryDuration+p.GenerateInterval)) } // NewRootKeys returns a root-keys cache that // is limited in size to approximately the given size. // // The NewStore method returns a store implementation // that uses specific store policy and backing database // implementation. // // If clock is non-nil, it will be used to find the current // time, otherwise time.Now will be used. func NewRootKeys(maxCacheSize int, clock Clock) *RootKeys { if clock == nil { clock = wallClock{} } return &RootKeys{ maxCacheSize: maxCacheSize, cache: make(map[string]RootKey), current: make(map[Policy]RootKey), clock: clock, } } // Policy holds a store policy for root keys. type Policy struct { // GenerateInterval holds the maximum length of time // for which a root key will be returned from RootKey. // If this is zero, it defaults to ExpiryDuration. GenerateInterval time.Duration // ExpiryDuration holds the minimum length of time that // root keys will be valid for after they are returned from // RootKey. The maximum length of time that they // will be valid for is ExpiryDuration + GenerateInterval. ExpiryDuration time.Duration } // NewStore returns a new RootKeyStore implementation that stores and // obtains root keys from the given Backing. If the given Backing also // implements ContextBacking, then the ContextBacking methods will be // used in preference. // // Root keys will be generated and stored following the // given store policy. // // It is expected that all Backing instances passed to a given Store's // NewStore method should refer to the same underlying database. func (s *RootKeys) NewStore(b Backing, policy Policy) bakery.RootKeyStore { cb, ok := b.(ContextBacking) if !ok { cb = backingWrapper{b: b} } return s.NewContextStore(cb, policy) } // NewContextStore returns a new RootKeyStore implementation that stores // and obtains root keys from the given ContextBacking. // // Root keys will be generated and stored following the // given store policy. // // It is expected that all ContextBacking instances passed to a given // Store's NewContextStore method should refer to the same underlying // database. func (s *RootKeys) NewContextStore(cb ContextBacking, policy Policy) bakery.RootKeyStore { if policy.GenerateInterval == 0 { policy.GenerateInterval = policy.ExpiryDuration } return &store{ keys: s, backing: cb, policy: policy, } } // get gets the root key for the given id, trying the cache first and // falling back to calling fallback if it's not found there. // // If the key does not exist or has expired, it returns // bakery.ErrNotFound. // // Called with s.mu locked. func (s *RootKeys) get(ctx context.Context, id []byte, b ContextBacking) (RootKey, error) { key, cached, err := s.get0(ctx, id, b) if err != nil && err != bakery.ErrNotFound { return RootKey{}, errgo.Mask(err) } if err == nil && s.clock.Now().After(key.Expires) { key = RootKey{} err = bakery.ErrNotFound } if !cached { s.addCache(id, key) } return key, err } // get0 is the inner version of RootKeys.get. It returns an item and reports // whether it was found in the cache, but doesn't check whether the // item has expired or move the returned item to s.cache. func (s *RootKeys) get0(ctx context.Context, id []byte, b ContextBacking) (key RootKey, inCache bool, err error) { if k, ok := s.cache[string(id)]; ok { if !k.IsValid() { return RootKey{}, true, bakery.ErrNotFound } return k, true, nil } if k, ok := s.oldCache[string(id)]; ok { if !k.IsValid() { return RootKey{}, false, bakery.ErrNotFound } return k, false, nil } k, err := b.GetKeyContext(ctx, id) return k, false, err } // addCache adds the given key to the cache. // Called with s.mu locked. func (s *RootKeys) addCache(id []byte, k RootKey) { if len(s.cache) >= s.maxCacheSize { s.oldCache = s.cache s.cache = make(map[string]RootKey) } s.cache[string(id)] = k } // setCurrent sets the current key for the given store policy. // Called with s.mu locked. func (s *RootKeys) setCurrent(policy Policy, key RootKey) { if len(s.current) > maxPolicyCache { // Sanity check to avoid possibly memory leak: // if some client is using arbitrarily many store // policies, we don't want s.keys.current to endlessly // expand, so just kill the cache if it grows too big. // This will result in worse performance but it shouldn't // happen in practice and it's better than using endless // space. s.current = make(map[Policy]RootKey) } s.current[policy] = key } type store struct { keys *RootKeys policy Policy backing ContextBacking } // Get implements bakery.RootKeyStore.Get. func (s *store) Get(ctx context.Context, id []byte) ([]byte, error) { s.keys.mu.Lock() defer s.keys.mu.Unlock() key, err := s.keys.get(ctx, id, s.backing) if err != nil { return nil, err } return key.RootKey, nil } // RootKey implements bakery.RootKeyStore.RootKey by // returning an existing key from the cache when compatible // with the current policy. func (s *store) RootKey(ctx context.Context) ([]byte, []byte, error) { if key := s.rootKeyFromCache(); key.IsValid() { return key.RootKey, key.Id, nil } // Try to find a root key from the collection. // It doesn't matter much if two concurrent mongo // clients are doing this at the same time because // we don't mind if there are more keys than necessary. // // Note that this query mirrors the logic found in // store.rootKeyFromCache. key, err := s.findBestRootKey(ctx) if err != nil { return nil, nil, errgo.Notef(err, "cannot query existing keys") } if !key.IsValid() { // No keys found anywhere, so let's create one. var err error key, err = s.generateKey() if err != nil { return nil, nil, errgo.Notef(err, "cannot generate key") } if err := s.backing.InsertKeyContext(ctx, key); err != nil { return nil, nil, errgo.Notef(err, "cannot create root key") } } s.keys.mu.Lock() defer s.keys.mu.Unlock() s.keys.addCache(key.Id, key) s.keys.setCurrent(s.policy, key) return key.RootKey, key.Id, nil } func (s *store) findBestRootKey(ctx context.Context) (RootKey, error) { now := s.keys.clock.Now() createdAfter := now.Add(-s.policy.GenerateInterval) expiresAfter := now.Add(s.policy.ExpiryDuration) expiresBefore := now.Add(s.policy.ExpiryDuration + s.policy.GenerateInterval) return s.backing.FindLatestKeyContext(ctx, createdAfter, expiresAfter, expiresBefore) } // rootKeyFromCache returns a root key from the cached keys. // If no keys are found that are valid for s.policy, it returns // the zero key. func (s *store) rootKeyFromCache() RootKey { s.keys.mu.Lock() defer s.keys.mu.Unlock() if k, ok := s.keys.current[s.policy]; ok && k.IsValidWithPolicy(s.policy, s.keys.clock.Now()) { return k } // Find the most recently created key that's consistent with the // store policy. var current RootKey for _, k := range s.keys.cache { if k.IsValidWithPolicy(s.policy, s.keys.clock.Now()) && k.Created.After(current.Created) { current = k } } if current.IsValid() { s.keys.current[s.policy] = current return current } return RootKey{} } func (s *store) generateKey() (RootKey, error) { newKey, err := randomBytes(24) if err != nil { return RootKey{}, err } newId, err := randomBytes(16) if err != nil { return RootKey{}, err } now := s.keys.clock.Now() return RootKey{ Created: now, Expires: now.Add(s.policy.ExpiryDuration + s.policy.GenerateInterval), // TODO return just newId when we know we can always // use non-text macaroon ids. Id: []byte(fmt.Sprintf("%x", newId)), RootKey: newKey, }, nil } func randomBytes(n int) ([]byte, error) { b := make([]byte, n) _, err := rand.Read(b) if err != nil { return nil, fmt.Errorf("cannot generate %d random bytes: %v", n, err) } return b, nil } // afterEq reports whether t0 is after or equal to t1. func afterEq(t0, t1 time.Time) bool { return !t0.Before(t1) } // beforeEq reports whether t1 is before or equal to t0. func beforeEq(t0, t1 time.Time) bool { return !t0.After(t1) } type wallClock struct{} func (wallClock) Now() time.Time { return time.Now() } macaroon-bakery-3.0.2/bakery/dbrootkeystore/rootkey_test.go000066400000000000000000000400571464427415500242260ustar00rootroot00000000000000package dbrootkeystore_test import ( "context" "testing" "time" qt "github.com/frankban/quicktest" "gopkg.in/errgo.v1" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/dbrootkeystore" ) var epoch = time.Date(2000, time.January, 1, 0, 0, 0, 0, time.UTC) var isValidWithPolicyTests = []struct { about string policy dbrootkeystore.Policy now time.Time key dbrootkeystore.RootKey expect bool }{{ about: "success", policy: dbrootkeystore.Policy{ GenerateInterval: 2 * time.Minute, ExpiryDuration: 3 * time.Minute, }, now: epoch.Add(20 * time.Minute), key: dbrootkeystore.RootKey{ Created: epoch.Add(19 * time.Minute), Expires: epoch.Add(24 * time.Minute), Id: []byte("id"), RootKey: []byte("key"), }, expect: true, }, { about: "empty root key", policy: dbrootkeystore.Policy{ GenerateInterval: 2 * time.Minute, ExpiryDuration: 3 * time.Minute, }, now: epoch.Add(20 * time.Minute), key: dbrootkeystore.RootKey{}, expect: false, }, { about: "created too early", policy: dbrootkeystore.Policy{ GenerateInterval: 2 * time.Minute, ExpiryDuration: 3 * time.Minute, }, now: epoch.Add(20 * time.Minute), key: dbrootkeystore.RootKey{ Created: epoch.Add(18*time.Minute - time.Millisecond), Expires: epoch.Add(24 * time.Minute), Id: []byte("id"), RootKey: []byte("key"), }, expect: false, }, { about: "expires too early", policy: dbrootkeystore.Policy{ GenerateInterval: 2 * time.Minute, ExpiryDuration: 3 * time.Minute, }, now: epoch.Add(20 * time.Minute), key: dbrootkeystore.RootKey{ Created: epoch.Add(19 * time.Minute), Expires: epoch.Add(21 * time.Minute), Id: []byte("id"), RootKey: []byte("key"), }, expect: false, }, { about: "expires too late", policy: dbrootkeystore.Policy{ GenerateInterval: 2 * time.Minute, ExpiryDuration: 3 * time.Minute, }, now: epoch.Add(20 * time.Minute), key: dbrootkeystore.RootKey{ Created: epoch.Add(19 * time.Minute), Expires: epoch.Add(25*time.Minute + time.Millisecond), Id: []byte("id"), RootKey: []byte("key"), }, expect: false, }} func TestIsValidWithPolicy(t *testing.T) { c := qt.New(t) for i, test := range isValidWithPolicyTests { c.Logf("test %d: %v", i, test.about) c.Assert(test.key.IsValidWithPolicy(test.policy, test.now), qt.Equals, test.expect) } } func TestRootKeyUsesKeysValidWithPolicy(t *testing.T) { c := qt.New(t) // We re-use the TestIsValidWithPolicy tests so that we // know that the database-backed logic uses the same behaviour. for i, test := range isValidWithPolicyTests { c.Logf("test %d: %v", i, test.about) if test.key.RootKey == nil { // We don't store empty root keys in the database. c.Logf("skipping test with empty root key") continue } // Prime the collection with the root key document. b := memBackingWithKeys([]dbrootkeystore.RootKey{test.key}) store := dbrootkeystore.NewRootKeys(10, stoppedClock(test.now)).NewStore(b, test.policy) key, id, err := store.RootKey(context.Background()) c.Assert(err, qt.Equals, nil) if test.expect { c.Assert(string(id), qt.Equals, "id") c.Assert(string(key), qt.Equals, "key") } else { // If it didn't match then RootKey will have // generated a new key. c.Assert(key, qt.HasLen, 24) c.Assert(id, qt.HasLen, 32) } } } func TestRootKey(t *testing.T) { c := qt.New(t) now := epoch clock := clockVal(&now) b := make(memBacking) store := dbrootkeystore.NewRootKeys(10, clock).NewStore(b, dbrootkeystore.Policy{ GenerateInterval: 2 * time.Minute, ExpiryDuration: 5 * time.Minute, }) key, id, err := store.RootKey(context.Background()) c.Assert(err, qt.Equals, nil) c.Assert(key, qt.HasLen, 24) c.Assert(id, qt.HasLen, 32) // If we get a key within the generate interval, we should // get the same one. now = epoch.Add(time.Minute) key1, id1, err := store.RootKey(context.Background()) c.Assert(err, qt.Equals, nil) c.Assert(key1, qt.DeepEquals, key) c.Assert(id1, qt.DeepEquals, id) // A different store instance should get the same root key. store1 := dbrootkeystore.NewRootKeys(10, clock).NewStore(b, dbrootkeystore.Policy{ GenerateInterval: 2 * time.Minute, ExpiryDuration: 5 * time.Minute, }) key1, id1, err = store1.RootKey(context.Background()) c.Assert(err, qt.Equals, nil) c.Assert(key1, qt.DeepEquals, key) c.Assert(id1, qt.DeepEquals, id) // After the generation interval has passed, we should generate a new key. now = epoch.Add(2*time.Minute + time.Second) key1, id1, err = store.RootKey(context.Background()) c.Assert(err, qt.Equals, nil) c.Assert(key, qt.HasLen, 24) c.Assert(id, qt.HasLen, 32) c.Assert(key1, qt.Not(qt.DeepEquals), key) c.Assert(id1, qt.Not(qt.DeepEquals), id) // The other store should pick it up too. key2, id2, err := store1.RootKey(context.Background()) c.Assert(err, qt.Equals, nil) c.Assert(key2, qt.DeepEquals, key1) c.Assert(id2, qt.DeepEquals, id1) } func TestRootKeyDefaultGenerateInterval(t *testing.T) { c := qt.New(t) now := epoch clock := clockVal(&now) b := make(memBacking) store := dbrootkeystore.NewRootKeys(10, clock).NewStore(b, dbrootkeystore.Policy{ ExpiryDuration: 5 * time.Minute, }) key, id, err := store.RootKey(context.Background()) c.Assert(err, qt.Equals, nil) now = epoch.Add(5 * time.Minute) key1, id1, err := store.RootKey(context.Background()) c.Assert(err, qt.Equals, nil) c.Assert(key1, qt.DeepEquals, key) c.Assert(id1, qt.DeepEquals, id) now = epoch.Add(5*time.Minute + time.Millisecond) key1, id1, err = store.RootKey(context.Background()) c.Assert(err, qt.Equals, nil) c.Assert(string(key1), qt.Not(qt.Equals), string(key)) c.Assert(string(id1), qt.Not(qt.Equals), string(id)) } var preferredRootKeyTests = []struct { about string now time.Time keys []dbrootkeystore.RootKey policy dbrootkeystore.Policy expectId string }{{ about: "latest creation time is preferred", now: epoch.Add(5 * time.Minute), keys: []dbrootkeystore.RootKey{{ Created: epoch.Add(4 * time.Minute), Expires: epoch.Add(15 * time.Minute), Id: []byte("id0"), RootKey: []byte("key0"), }, { Created: epoch.Add(5*time.Minute + 30*time.Second), Expires: epoch.Add(16 * time.Minute), Id: []byte("id1"), RootKey: []byte("key1"), }, { Created: epoch.Add(5 * time.Minute), Expires: epoch.Add(16 * time.Minute), Id: []byte("id2"), RootKey: []byte("key2"), }}, policy: dbrootkeystore.Policy{ GenerateInterval: 5 * time.Minute, ExpiryDuration: 7 * time.Minute, }, expectId: "id1", }, { about: "ineligible keys are exluded", now: epoch.Add(5 * time.Minute), keys: []dbrootkeystore.RootKey{{ Created: epoch.Add(4 * time.Minute), Expires: epoch.Add(15 * time.Minute), Id: []byte("id0"), RootKey: []byte("key0"), }, { Created: epoch.Add(5 * time.Minute), Expires: epoch.Add(16*time.Minute + 30*time.Second), Id: []byte("id1"), RootKey: []byte("key1"), }, { Created: epoch.Add(6 * time.Minute), Expires: epoch.Add(time.Hour), Id: []byte("id2"), RootKey: []byte("key2"), }}, policy: dbrootkeystore.Policy{ GenerateInterval: 5 * time.Minute, ExpiryDuration: 7 * time.Minute, }, expectId: "id1", }} func TestPreferredRootKeyFromDatabase(t *testing.T) { c := qt.New(t) for i, test := range preferredRootKeyTests { c.Logf("%d: %v", i, test.about) b := memBackingWithKeys(test.keys) store := dbrootkeystore.NewRootKeys(10, stoppedClock(test.now)).NewStore(b, test.policy) _, id, err := store.RootKey(context.Background()) c.Assert(err, qt.Equals, nil) c.Assert(string(id), qt.DeepEquals, test.expectId) } } func TestPreferredRootKeyFromCache(t *testing.T) { c := qt.New(t) for i, test := range preferredRootKeyTests { c.Logf("%d: %v", i, test.about) b := memBackingWithKeys(test.keys) store := dbrootkeystore.NewRootKeys(10, stoppedClock(test.now)).NewStore(b, test.policy) // Ensure that all the keys are in cache by getting all of them. for _, key := range test.keys { got, err := store.Get(context.Background(), key.Id) c.Assert(err, qt.Equals, nil) c.Assert(got, qt.DeepEquals, key.RootKey) } // Remove all the keys from the collection so that // we know we must be acquiring them from the cache. for id := range b { delete(b, id) } // Test that RootKey returns the expected key. _, id, err := store.RootKey(context.Background()) c.Assert(err, qt.Equals, nil) c.Assert(string(id), qt.DeepEquals, test.expectId) } } func TestGet(t *testing.T) { c := qt.New(t) now := epoch clock := clockVal(&now) mb := make(memBacking) b := &funcBacking{Backing: mb} store := dbrootkeystore.NewRootKeys(5, clock).NewStore(b, dbrootkeystore.Policy{ GenerateInterval: 1 * time.Minute, ExpiryDuration: 30 * time.Minute, }) type idKey struct { id string key []byte } var keys []idKey keyIds := make(map[string]bool) for i := 0; i < 20; i++ { key, id, err := store.RootKey(context.Background()) c.Assert(err, qt.Equals, nil) c.Assert(keyIds[string(id)], qt.Equals, false) keys = append(keys, idKey{string(id), key}) now = now.Add(time.Minute + time.Second) } for i, k := range keys { key, err := store.Get(context.Background(), []byte(k.id)) c.Assert(err, qt.Equals, nil, qt.Commentf("key %d (%s)", i, k.id)) c.Assert(key, qt.DeepEquals, k.key, qt.Commentf("key %d (%s)", i, k.id)) } // Check that the keys are cached. // // Since the cache size is 5, the most recent 5 items will be in // the primary cache; the 5 items before that will be in the old // cache and nothing else will be cached. // // The first time we fetch an item from the old cache, a new // primary cache will be allocated, all existing items in the // old cache except that item will be evicted, and all items in // the current primary cache moved to the old cache. // // The upshot of that is that all but the first 6 calls to Get // should result in a database fetch. var fetched []string b.getKey = func(id []byte) (dbrootkeystore.RootKey, error) { fetched = append(fetched, string(id)) return mb.GetKey(id) } c.Logf("testing cache") for i := len(keys) - 1; i >= 0; i-- { k := keys[i] key, err := store.Get(context.Background(), []byte(k.id)) c.Assert(err, qt.Equals, nil) c.Assert(err, qt.Equals, nil, qt.Commentf("key %d (%s)", i, k.id)) c.Assert(key, qt.DeepEquals, k.key, qt.Commentf("key %d (%s)", i, k.id)) } c.Assert(len(fetched), qt.Equals, len(keys)-6) for i, id := range fetched { c.Assert(id, qt.Equals, keys[len(keys)-6-i-1].id) } } func TestGetCachesMisses(t *testing.T) { c := qt.New(t) var fetched []string mb := make(memBacking) b := &funcBacking{ Backing: mb, getKey: func(id []byte) (dbrootkeystore.RootKey, error) { fetched = append(fetched, string(id)) return mb.GetKey(id) }, } store := dbrootkeystore.NewRootKeys(5, nil).NewStore(b, dbrootkeystore.Policy{ GenerateInterval: 1 * time.Minute, ExpiryDuration: 30 * time.Minute, }) key, err := store.Get(context.Background(), []byte("foo")) c.Assert(err, qt.Equals, bakery.ErrNotFound) c.Assert(key, qt.IsNil) c.Assert(fetched, qt.DeepEquals, []string{"foo"}) fetched = nil key, err = store.Get(context.Background(), []byte("foo")) c.Assert(err, qt.Equals, bakery.ErrNotFound) c.Assert(key, qt.IsNil) c.Assert(fetched, qt.IsNil) } func TestGetExpiredItemFromCache(t *testing.T) { c := qt.New(t) now := epoch clock := clockVal(&now) b := &funcBacking{ Backing: make(memBacking), } store := dbrootkeystore.NewRootKeys(10, clock).NewStore(b, dbrootkeystore.Policy{ ExpiryDuration: 5 * time.Minute, }) _, id, err := store.RootKey(context.Background()) c.Assert(err, qt.Equals, nil) b.getKey = func(id []byte) (dbrootkeystore.RootKey, error) { c.Errorf("GetKey unexpectedly called") return dbrootkeystore.RootKey{}, errgo.New("unexpected call to GetKey") } now = epoch.Add(15 * time.Minute) _, err = store.Get(context.Background(), id) c.Assert(err, qt.Equals, bakery.ErrNotFound) } func TestContextBackingTakesPrecedence(t *testing.T) { c := qt.New(t) b := fullContextBacking{make(memBacking)} store := dbrootkeystore.NewRootKeys(5, nil).NewStore(b, dbrootkeystore.Policy{ GenerateInterval: 1 * time.Minute, ExpiryDuration: 30 * time.Minute, }) ctx := context.Background() key1, id, err := store.RootKey(ctx) c.Assert(err, qt.Equals, nil) key2, err := store.Get(ctx, id) c.Assert(err, qt.Equals, nil) c.Assert(key1, qt.DeepEquals, key2) } type fullContextBacking struct { b dbrootkeystore.Backing } func (b fullContextBacking) GetKey(id []byte) (dbrootkeystore.RootKey, error) { return dbrootkeystore.RootKey{}, errgo.Newf("Unexected call to GetKey") } func (b fullContextBacking) GetKeyContext(_ context.Context, id []byte) (dbrootkeystore.RootKey, error) { return b.b.GetKey(id) } func (b fullContextBacking) FindLatestKey(createdAfter, expiresAfter, expiresBefore time.Time) (dbrootkeystore.RootKey, error) { return dbrootkeystore.RootKey{}, errgo.Newf("Unexected call to FindLatestKey") } func (b fullContextBacking) FindLatestKeyContext(_ context.Context, createdAfter, expiresAfter, expiresBefore time.Time) (dbrootkeystore.RootKey, error) { return b.b.FindLatestKey(createdAfter, expiresAfter, expiresBefore) } func (b fullContextBacking) InsertKey(_ dbrootkeystore.RootKey) error { return errgo.Newf("Unexected call to FindLatestKey") } func (b fullContextBacking) InsertKeyContext(_ context.Context, key dbrootkeystore.RootKey) error { return b.b.InsertKey(key) } func TestContextBackingStore(t *testing.T) { c := qt.New(t) b := contextBacking{make(memBacking)} store := dbrootkeystore.NewRootKeys(5, nil).NewContextStore(b, dbrootkeystore.Policy{ GenerateInterval: 1 * time.Minute, ExpiryDuration: 30 * time.Minute, }) ctx := context.Background() key1, id, err := store.RootKey(ctx) c.Assert(err, qt.Equals, nil) key2, err := store.Get(ctx, id) c.Assert(err, qt.Equals, nil) c.Assert(key1, qt.DeepEquals, key2) } type contextBacking struct { b dbrootkeystore.Backing } func (b contextBacking) GetKeyContext(_ context.Context, id []byte) (dbrootkeystore.RootKey, error) { return b.b.GetKey(id) } func (b contextBacking) FindLatestKeyContext(_ context.Context, createdAfter, expiresAfter, expiresBefore time.Time) (dbrootkeystore.RootKey, error) { return b.b.FindLatestKey(createdAfter, expiresAfter, expiresBefore) } func (b contextBacking) InsertKeyContext(_ context.Context, key dbrootkeystore.RootKey) error { return b.b.InsertKey(key) } func memBackingWithKeys(keys []dbrootkeystore.RootKey) memBacking { b := make(memBacking) for _, key := range keys { err := b.InsertKey(key) if err != nil { panic(err) } } return b } type funcBacking struct { dbrootkeystore.Backing getKey func(id []byte) (dbrootkeystore.RootKey, error) } func (b *funcBacking) GetKey(id []byte) (dbrootkeystore.RootKey, error) { if b.getKey == nil { return b.Backing.GetKey(id) } return b.getKey(id) } type memBacking map[string]dbrootkeystore.RootKey func (b memBacking) GetKey(id []byte) (dbrootkeystore.RootKey, error) { key, ok := b[string(id)] if !ok { return dbrootkeystore.RootKey{}, bakery.ErrNotFound } return key, nil } func (b memBacking) FindLatestKey(createdAfter, expiresAfter, expiresBefore time.Time) (dbrootkeystore.RootKey, error) { var best dbrootkeystore.RootKey for _, k := range b { if afterEq(k.Created, createdAfter) && afterEq(k.Expires, expiresAfter) && beforeEq(k.Expires, expiresBefore) && k.Created.After(best.Created) { best = k } } return best, nil } func (b memBacking) InsertKey(key dbrootkeystore.RootKey) error { if _, ok := b[string(key.Id)]; ok { return errgo.Newf("duplicate key") } b[string(key.Id)] = key return nil } func clockVal(t *time.Time) dbrootkeystore.Clock { return clockFunc(func() time.Time { return *t }) } type clockFunc func() time.Time func (f clockFunc) Now() time.Time { return f() } // afterEq reports whether t0 is after or equal to t1. func afterEq(t0, t1 time.Time) bool { return !t0.Before(t1) } // beforeEq reports whether t1 is before or equal to t0. func beforeEq(t0, t1 time.Time) bool { return !t0.After(t1) } func stoppedClock(t time.Time) dbrootkeystore.Clock { return clockFunc(func() time.Time { return t }) } macaroon-bakery-3.0.2/bakery/discharge.go000066400000000000000000000215671464427415500203520ustar00rootroot00000000000000package bakery import ( "context" "crypto/rand" "fmt" "strconv" "strings" "gopkg.in/errgo.v1" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/checkers" ) // LocalThirdPartyCaveat returns a third-party caveat that, when added // to a macaroon with AddCaveat, results in a caveat // with the location "local", encrypted with the given public key. // This can be automatically discharged by DischargeAllWithKey. func LocalThirdPartyCaveat(key *PublicKey, version Version) checkers.Caveat { var loc string if version < Version2 { loc = "local " + key.String() } else { loc = fmt.Sprintf("local %d %s", version, key) } return checkers.Caveat{ Location: loc, } } // parseLocalLocation parses a local caveat location as generated by // LocalThirdPartyCaveat. This is of the form: // // local // // where is the bakery version of the client that we're // adding the local caveat for. // // It returns false if the location does not represent a local // caveat location. func parseLocalLocation(loc string) (ThirdPartyInfo, bool) { if !strings.HasPrefix(loc, "local ") { return ThirdPartyInfo{}, false } version := Version1 fields := strings.Fields(loc) fields = fields[1:] // Skip "local" switch len(fields) { case 2: v, err := strconv.Atoi(fields[0]) if err != nil { return ThirdPartyInfo{}, false } version = Version(v) fields = fields[1:] fallthrough case 1: var key PublicKey if err := key.UnmarshalText([]byte(fields[0])); err != nil { return ThirdPartyInfo{}, false } return ThirdPartyInfo{ PublicKey: key, Version: version, }, true default: return ThirdPartyInfo{}, false } } // DischargeParams holds parameters for a Discharge call. type DischargeParams struct { // Id holds the id to give to the discharge macaroon. // If Caveat is empty, then the id also holds the // encrypted third party caveat. Id []byte // Caveat holds the encrypted third party caveat. If this // is nil, Id will be used. Caveat []byte // Key holds the key to use to decrypt the third party // caveat information and to encrypt any additional // third party caveats returned by the caveat checker. Key *KeyPair // Checker is used to check the third party caveat, // and may also return further caveats to be added to // the discharge macaroon. Checker ThirdPartyCaveatChecker // Locator is used to information on third parties // referred to by third party caveats returned by the Checker. Locator ThirdPartyLocator } // Discharge creates a macaroon to discharges a third party caveat. // The given parameters specify the caveat and how it should be checked/ // // The condition implicit in the caveat is checked for validity using p.Checker. If // it is valid, a new macaroon is returned which discharges the caveat. // // The macaroon is created with a version derived from the version // that was used to encode the id. func Discharge(ctx context.Context, p DischargeParams) (*Macaroon, error) { var caveatIdPrefix []byte if p.Caveat == nil { // The caveat information is encoded in the id itself. p.Caveat = p.Id } else { // We've been given an explicit id, so when extra third party // caveats are added, use that id as the prefix // for any more ids. caveatIdPrefix = p.Id } cavInfo, err := decodeCaveat(p.Key, p.Caveat) if err != nil { return nil, errgo.Notef(err, "discharger cannot decode caveat id") } cavInfo.Id = p.Id // Note that we don't check the error - we allow the // third party checker to see even caveats that we can't // understand. cond, arg, _ := checkers.ParseCaveat(string(cavInfo.Condition)) var caveats []checkers.Caveat if cond == checkers.CondNeedDeclared { cavInfo.Condition = []byte(arg) caveats, err = checkNeedDeclared(ctx, cavInfo, p.Checker) } else { caveats, err = p.Checker.CheckThirdPartyCaveat(ctx, cavInfo) } if err != nil { return nil, errgo.Mask(err, errgo.Any) } // Note that the discharge macaroon does not need to // be stored persistently. Indeed, it would be a problem if // we did, because then the macaroon could potentially be used // for normal authorization with the third party. m, err := NewMacaroon(cavInfo.RootKey, p.Id, "", cavInfo.Version, cavInfo.Namespace) if err != nil { return nil, errgo.Mask(err) } m.caveatIdPrefix = caveatIdPrefix for _, cav := range caveats { if err := m.AddCaveat(ctx, cav, p.Key, p.Locator); err != nil { return nil, errgo.Notef(err, "could not add caveat") } } return m, nil } func checkNeedDeclared(ctx context.Context, cavInfo *ThirdPartyCaveatInfo, checker ThirdPartyCaveatChecker) ([]checkers.Caveat, error) { arg := string(cavInfo.Condition) i := strings.Index(arg, " ") if i <= 0 { return nil, errgo.Newf("need-declared caveat requires an argument, got %q", arg) } needDeclared := strings.Split(arg[0:i], ",") for _, d := range needDeclared { if d == "" { return nil, errgo.New("need-declared caveat with empty required attribute") } } if len(needDeclared) == 0 { return nil, fmt.Errorf("need-declared caveat with no required attributes") } cavInfo.Condition = []byte(arg[i+1:]) caveats, err := checker.CheckThirdPartyCaveat(ctx, cavInfo) if err != nil { return nil, errgo.Mask(err, errgo.Any) } declared := make(map[string]bool) for _, cav := range caveats { if cav.Location != "" { continue } // Note that we ignore the error. We allow the service to // generate caveats that we don't understand here. cond, arg, _ := checkers.ParseCaveat(cav.Condition) if cond != checkers.CondDeclared { continue } parts := strings.SplitN(arg, " ", 2) if len(parts) != 2 { return nil, errgo.Newf("declared caveat has no value") } declared[parts[0]] = true } // Add empty declarations for everything mentioned in need-declared // that was not actually declared. for _, d := range needDeclared { if !declared[d] { caveats = append(caveats, checkers.DeclaredCaveat(d, "")) } } return caveats, nil } func randomBytes(n int) ([]byte, error) { b := make([]byte, n) _, err := rand.Read(b) if err != nil { return nil, fmt.Errorf("cannot generate %d random bytes: %v", n, err) } return b, nil } // ThirdPartyCaveatInfo holds the information decoded from // a third party caveat id. type ThirdPartyCaveatInfo struct { // Condition holds the third party condition to be discharged. // This is the only field that most third party dischargers will // need to consider. Condition []byte // FirstPartyPublicKey holds the public key of the party // that created the third party caveat. FirstPartyPublicKey PublicKey // ThirdPartyKeyPair holds the key pair used to decrypt // the caveat - the key pair of the discharging service. ThirdPartyKeyPair KeyPair // RootKey holds the secret root key encoded by the caveat. RootKey []byte // CaveatId holds the full encoded caveat id from which all // the other fields are derived. Caveat []byte // Version holds the version that was used to encode // the caveat id. Version Version // Id holds the id of the third party caveat (the id that // the discharge macaroon should be given). This // will differ from Caveat when the caveat information // is encoded separately. Id []byte // Namespace holds the namespace of the first party // that created the macaroon, as encoded by the party // that added the third party caveat. Namespace *checkers.Namespace } // ThirdPartyCaveatChecker holds a function that checks third party caveats // for validity. If the caveat is valid, it returns a nil error and // optionally a slice of extra caveats that will be added to the // discharge macaroon. The caveatId parameter holds the still-encoded id // of the caveat. // // If the caveat kind was not recognised, the checker should return an // error with a ErrCaveatNotRecognized cause. type ThirdPartyCaveatChecker interface { CheckThirdPartyCaveat(ctx context.Context, info *ThirdPartyCaveatInfo) ([]checkers.Caveat, error) } // ThirdPartyCaveatCheckerFunc implements ThirdPartyCaveatChecker by calling a function. type ThirdPartyCaveatCheckerFunc func(context.Context, *ThirdPartyCaveatInfo) ([]checkers.Caveat, error) // CheckThirdPartyCaveat implements ThirdPartyCaveatChecker.CheckThirdPartyCaveat by calling // the receiver with the given arguments func (c ThirdPartyCaveatCheckerFunc) CheckThirdPartyCaveat(ctx context.Context, info *ThirdPartyCaveatInfo) ([]checkers.Caveat, error) { return c(ctx, info) } // FirstPartyCaveatChecker is used to check first party caveats // for validity with respect to information in the provided context. // // If the caveat kind was not recognised, the checker should return // ErrCaveatNotRecognized. type FirstPartyCaveatChecker interface { // CheckFirstPartyCaveat checks that the given caveat condition // is valid with respect to the given context information. CheckFirstPartyCaveat(ctx context.Context, caveat string) error // Namespace returns the namespace associated with the // caveat checker. Namespace() *checkers.Namespace } macaroon-bakery-3.0.2/bakery/discharge_test.go000066400000000000000000000376321464427415500214110ustar00rootroot00000000000000package bakery_test import ( "context" "fmt" "testing" "unicode/utf8" qt "github.com/frankban/quicktest" "gopkg.in/errgo.v1" "gopkg.in/macaroon.v2" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/checkers" ) // TestSingleServiceFirstParty creates a single service // with a macaroon with one first party caveat. // It creates a request with this macaroon and checks that the service // can verify this macaroon as valid. func TestSingleServiceFirstParty(t *testing.T) { c := qt.New(t) oc := newBakery("bakerytest", nil) primary, err := oc.Oven.NewMacaroon(testContext, bakery.LatestVersion, nil, basicOp) c.Assert(err, qt.IsNil) c.Assert(primary.M().Location(), qt.Equals, "bakerytest") err = oc.Oven.AddCaveat(testContext, primary, strCaveat("something")) _, err = oc.Checker.Auth(macaroon.Slice{primary.M()}).Allow(strContext("something"), basicOp) c.Assert(err, qt.IsNil) } // TestMacaroonPaperFig6 implements an example flow as described in the macaroons paper: // http://theory.stanford.edu/~ataly/Papers/macaroons.pdf // There are three services, ts, fs, as: // ts is a store service which has deligated authority to a forum service fs. // The forum service wants to require its users to be logged into to an authentication service as. // // The client obtains a macaroon from fs (minted by ts, with a third party caveat addressed to as). // The client obtains a discharge macaroon from as to satisfy this caveat. // The target service verifies the original macaroon it delegated to fs // No direct contact between as and ts is required func TestMacaroonPaperFig6(t *testing.T) { c := qt.New(t) locator := bakery.NewThirdPartyStore() as := newBakery("as-loc", locator) ts := newBakery("ts-loc", locator) fs := newBakery("fs-loc", locator) // ts creates a macaroon. tsMacaroon, err := ts.Oven.NewMacaroon(testContext, bakery.LatestVersion, nil, basicOp) c.Assert(err, qt.IsNil) // ts somehow sends the macaroon to fs which adds a third party caveat to be discharged by as. err = fs.Oven.AddCaveat(testContext, tsMacaroon, checkers.Caveat{Location: "as-loc", Condition: "user==bob"}) c.Assert(err, qt.IsNil) // client asks for a discharge macaroon for each third party caveat d, err := bakery.DischargeAll(testContext, tsMacaroon, func(ctx context.Context, cav macaroon.Caveat, payload []byte) (*bakery.Macaroon, error) { c.Assert(cav.Location, qt.Equals, "as-loc") return discharge(ctx, as.Oven, thirdPartyStrcmpChecker("user==bob"), cav, payload) }) c.Assert(err, qt.IsNil) _, err = ts.Checker.Auth(d).Allow(testContext, basicOp) c.Assert(err, qt.IsNil) } func TestDischargeWithVersion1Macaroon(t *testing.T) { c := qt.New(t) locator := bakery.NewThirdPartyStore() as := newBakery("as-loc", locator) ts := newBakery("ts-loc", locator) // ts creates a old-version macaroon. tsMacaroon, err := ts.Oven.NewMacaroon(testContext, bakery.Version1, nil, basicOp) c.Assert(err, qt.IsNil) err = ts.Oven.AddCaveat(testContext, tsMacaroon, checkers.Caveat{Location: "as-loc", Condition: "something"}) c.Assert(err, qt.IsNil) // client asks for a discharge macaroon for each third party caveat d, err := bakery.DischargeAll(testContext, tsMacaroon, func(ctx context.Context, cav macaroon.Caveat, payload []byte) (*bakery.Macaroon, error) { // Make sure that the caveat id really is old-style. c.Assert(cav.Id, qt.Satisfies, utf8.Valid) return discharge(ctx, as.Oven, thirdPartyStrcmpChecker("something"), cav, payload) }) c.Assert(err, qt.IsNil) _, err = ts.Checker.Auth(d).Allow(testContext, basicOp) c.Assert(err, qt.IsNil) for _, m := range d { c.Assert(m.Version(), qt.Equals, macaroon.V1) } } func TestVersion1MacaroonId(t *testing.T) { c := qt.New(t) // In the version 1 bakery, macaroon ids were hex-encoded with a hyphenated // UUID suffix. rootKeyStore := bakery.NewMemRootKeyStore() b := bakery.New(bakery.BakeryParams{ RootKeyStore: rootKeyStore, LegacyMacaroonOp: basicOp, }) key, id, err := rootKeyStore.RootKey(testContext) c.Assert(err, qt.IsNil) _, err = rootKeyStore.Get(testContext, id) c.Assert(err, qt.IsNil) m, err := macaroon.New(key, []byte(fmt.Sprintf("%s-deadl00f", id)), "", macaroon.V1) c.Assert(err, qt.IsNil) _, err = b.Checker.Auth(macaroon.Slice{m}).Allow(testContext, basicOp) c.Assert(err, qt.IsNil) } // TestMacaroonPaperFig6FailsWithoutDischarges runs a similar test as TestMacaroonPaperFig6 // without the client discharging the third party caveats. func TestMacaroonPaperFig6FailsWithoutDischarges(t *testing.T) { c := qt.New(t) locator := bakery.NewThirdPartyStore() ts := newBakery("ts-loc", locator) fs := newBakery("fs-loc", locator) newBakery("as-loc", locator) // ts creates a macaroon. tsMacaroon, err := ts.Oven.NewMacaroon(testContext, bakery.LatestVersion, nil, basicOp) c.Assert(err, qt.IsNil) // ts somehow sends the macaroon to fs which adds a third party caveat to be discharged by as. err = fs.Oven.AddCaveat(testContext, tsMacaroon, checkers.Caveat{Location: "as-loc", Condition: "user==bob"}) c.Assert(err, qt.IsNil) // client makes request to ts _, err = ts.Checker.Auth(macaroon.Slice{tsMacaroon.M()}).Allow(testContext, basicOp) c.Assert(err, qt.ErrorMatches, `verification failed: cannot find discharge macaroon for caveat .*`, qt.Commentf("%#v", err)) } // TestMacaroonPaperFig6FailsWithBindingOnTamperedSignature runs a similar test as TestMacaroonPaperFig6 // with the discharge macaroon binding being done on a tampered signature. func TestMacaroonPaperFig6FailsWithBindingOnTamperedSignature(t *testing.T) { c := qt.New(t) locator := bakery.NewThirdPartyStore() as := newBakery("as-loc", locator) ts := newBakery("ts-loc", locator) fs := newBakery("fs-loc", locator) // ts creates a macaroon. tsMacaroon, err := ts.Oven.NewMacaroon(testContext, bakery.LatestVersion, nil, basicOp) c.Assert(err, qt.IsNil) // ts somehow sends the macaroon to fs which adds a third party caveat to be discharged by as. err = fs.Oven.AddCaveat(testContext, tsMacaroon, checkers.Caveat{Location: "as-loc", Condition: "user==bob"}) c.Assert(err, qt.IsNil) // client asks for a discharge macaroon for each third party caveat d, err := bakery.DischargeAll(testContext, tsMacaroon, func(ctx context.Context, cav macaroon.Caveat, payload []byte) (*bakery.Macaroon, error) { c.Assert(cav.Location, qt.Equals, "as-loc") return discharge(ctx, as.Oven, thirdPartyStrcmpChecker("user==bob"), cav, payload) }) c.Assert(err, qt.IsNil) // client has all the discharge macaroons. For each discharge macaroon bind it to our tsMacaroon // and add it to our request. for _, dm := range d[1:] { dm.Bind([]byte("tampered-signature")) // Bind against an incorrect signature. } // client makes request to ts. _, err = ts.Checker.Auth(d).Allow(testContext, basicOp) // TODO fix this error message. c.Assert(err, qt.ErrorMatches, "verification failed: signature mismatch after caveat verification") } func discharge(ctx context.Context, oven *bakery.Oven, checker bakery.ThirdPartyCaveatChecker, cav macaroon.Caveat, payload []byte) (*bakery.Macaroon, error) { return bakery.Discharge(ctx, bakery.DischargeParams{ Key: oven.Key(), Locator: oven.Locator(), Id: cav.Id, Caveat: payload, Checker: checker, }) } func TestNeedDeclared(t *testing.T) { c := qt.New(t) locator := bakery.NewThirdPartyStore() firstParty := newBakery("first", locator) thirdParty := newBakery("third", locator) // firstParty mints a macaroon with a third-party caveat addressed // to thirdParty with a need-declared caveat. m, err := firstParty.Oven.NewMacaroon(testContext, bakery.LatestVersion, []checkers.Caveat{ checkers.NeedDeclaredCaveat(checkers.Caveat{ Location: "third", Condition: "something", }, "foo", "bar"), }, basicOp) c.Assert(err, qt.IsNil) // The client asks for a discharge macaroon for each third party caveat. d, err := bakery.DischargeAll(testContext, m, func(ctx context.Context, cav macaroon.Caveat, payload []byte) (*bakery.Macaroon, error) { return discharge(ctx, thirdParty.Oven, thirdPartyStrcmpChecker("something"), cav, payload) }) c.Assert(err, qt.IsNil) // The required declared attributes should have been added // to the discharge macaroons. declared := checkers.InferDeclared(firstParty.Checker.Namespace(), d) c.Assert(declared, qt.DeepEquals, map[string]string{ "foo": "", "bar": "", }) // Make sure the macaroons actually check out correctly // when provided with the declared checker. ctx := checkers.ContextWithMacaroons(testContext, firstParty.Checker.Namespace(), d) _, err = firstParty.Checker.Auth(d).Allow(ctx, basicOp) c.Assert(err, qt.IsNil) // Try again when the third party does add a required declaration. // The client asks for a discharge macaroon for each third party caveat. d, err = bakery.DischargeAll(testContext, m, func(ctx context.Context, cav macaroon.Caveat, payload []byte) (*bakery.Macaroon, error) { checker := thirdPartyCheckerWithCaveats{ checkers.DeclaredCaveat("foo", "a"), checkers.DeclaredCaveat("arble", "b"), } return discharge(ctx, thirdParty.Oven, checker, cav, payload) }) c.Assert(err, qt.IsNil) // One attribute should have been added, the other was already there. declared = checkers.InferDeclared(firstParty.Checker.Namespace(), d) c.Assert(declared, qt.DeepEquals, map[string]string{ "foo": "a", "bar": "", "arble": "b", }) ctx = checkers.ContextWithMacaroons(testContext, firstParty.Checker.Namespace(), d) _, err = firstParty.Checker.Auth(d).Allow(ctx, basicOp) c.Assert(err, qt.IsNil) // Try again, but this time pretend a client is sneakily trying // to add another "declared" attribute to alter the declarations. d, err = bakery.DischargeAll(testContext, m, func(ctx context.Context, cav macaroon.Caveat, payload []byte) (*bakery.Macaroon, error) { checker := thirdPartyCheckerWithCaveats{ checkers.DeclaredCaveat("foo", "a"), checkers.DeclaredCaveat("arble", "b"), } // Sneaky client adds a first party caveat. m, err := discharge(ctx, thirdParty.Oven, checker, cav, payload) c.Assert(err, qt.IsNil) err = m.AddCaveat(ctx, checkers.DeclaredCaveat("foo", "c"), nil, nil) c.Assert(err, qt.IsNil) return m, nil }) c.Assert(err, qt.IsNil) declared = checkers.InferDeclared(firstParty.Checker.Namespace(), d) c.Assert(declared, qt.DeepEquals, map[string]string{ "bar": "", "arble": "b", }) ctx = checkers.ContextWithMacaroons(testContext, firstParty.Checker.Namespace(), d) _, err = firstParty.Checker.Auth(d).Allow(testContext, basicOp) c.Assert(err, qt.ErrorMatches, `caveat "declared foo a" not satisfied: got foo=null, expected "a"`) } func TestDischargeTwoNeedDeclared(t *testing.T) { c := qt.New(t) locator := bakery.NewThirdPartyStore() firstParty := newBakery("first", locator) thirdParty := newBakery("third", locator) // firstParty mints a macaroon with two third party caveats // with overlapping attributes. m, err := firstParty.Oven.NewMacaroon(testContext, bakery.LatestVersion, []checkers.Caveat{ checkers.NeedDeclaredCaveat(checkers.Caveat{ Location: "third", Condition: "x", }, "foo", "bar"), checkers.NeedDeclaredCaveat(checkers.Caveat{ Location: "third", Condition: "y", }, "bar", "baz"), }, basicOp) c.Assert(err, qt.IsNil) // The client asks for a discharge macaroon for each third party caveat. // Since no declarations are added by the discharger, d, err := bakery.DischargeAll(testContext, m, func(ctx context.Context, cav macaroon.Caveat, payload []byte) (*bakery.Macaroon, error) { return discharge(ctx, thirdParty.Oven, bakery.ThirdPartyCaveatCheckerFunc(func(context.Context, *bakery.ThirdPartyCaveatInfo) ([]checkers.Caveat, error) { return nil, nil }), cav, payload) }) c.Assert(err, qt.IsNil) declared := checkers.InferDeclared(firstParty.Checker.Namespace(), d) c.Assert(declared, qt.DeepEquals, map[string]string{ "foo": "", "bar": "", "baz": "", }) ctx := checkers.ContextWithMacaroons(testContext, firstParty.Checker.Namespace(), d) _, err = firstParty.Checker.Auth(d).Allow(ctx, basicOp) c.Assert(err, qt.IsNil) // If they return conflicting values, the discharge fails. // The client asks for a discharge macaroon for each third party caveat. // Since no declarations are added by the discharger, d, err = bakery.DischargeAll(testContext, m, func(ctx context.Context, cav macaroon.Caveat, payload []byte) (*bakery.Macaroon, error) { return discharge(ctx, thirdParty.Oven, bakery.ThirdPartyCaveatCheckerFunc(func(_ context.Context, cavInfo *bakery.ThirdPartyCaveatInfo) ([]checkers.Caveat, error) { switch string(cavInfo.Condition) { case "x": return []checkers.Caveat{ checkers.DeclaredCaveat("foo", "fooval1"), }, nil case "y": return []checkers.Caveat{ checkers.DeclaredCaveat("foo", "fooval2"), checkers.DeclaredCaveat("baz", "bazval"), }, nil } return nil, fmt.Errorf("not matched") }), cav, payload) }) c.Assert(err, qt.IsNil) declared = checkers.InferDeclared(firstParty.Checker.Namespace(), d) c.Assert(declared, qt.DeepEquals, map[string]string{ "bar": "", "baz": "bazval", }) ctx = checkers.ContextWithMacaroons(testContext, firstParty.Checker.Namespace(), d) _, err = firstParty.Checker.Auth(d).Allow(testContext, basicOp) c.Assert(err, qt.ErrorMatches, `caveat "declared foo fooval1" not satisfied: got foo=null, expected "fooval1"`) } func TestDischargeMacaroonCannotBeUsedAsNormalMacaroon(t *testing.T) { c := qt.New(t) locator := bakery.NewThirdPartyStore() firstParty := newBakery("first", locator) thirdParty := newBakery("third", locator) // First party mints a macaroon with a 3rd party caveat. m, err := firstParty.Oven.NewMacaroon(testContext, bakery.LatestVersion, []checkers.Caveat{{ Location: "third", Condition: "true", }}, basicOp) c.Assert(err, qt.IsNil) // Acquire the discharge macaroon, but don't bind it to the original. var unbound *macaroon.Macaroon _, err = bakery.DischargeAll(testContext, m, func(ctx context.Context, cav macaroon.Caveat, payload []byte) (*bakery.Macaroon, error) { m, err := discharge(ctx, thirdParty.Oven, thirdPartyStrcmpChecker("true"), cav, payload) if err == nil { unbound = m.M().Clone() } return m, err }) c.Assert(err, qt.IsNil) c.Assert(unbound, qt.Not(qt.IsNil)) // Make sure it cannot be used as a normal macaroon in the third party. _, err = thirdParty.Checker.Auth(macaroon.Slice{unbound}).Allow(testContext, basicOp) c.Assert(err, qt.ErrorMatches, `cannot retrieve macaroon: cannot unmarshal macaroon id: .*`) } func TestThirdPartyDischargeMacaroonIdsAreSmall(t *testing.T) { c := qt.New(t) locator := bakery.NewThirdPartyStore() bakeries := map[string]*bakery.Bakery{ "ts-loc": newBakery("ts-loc", locator), "as1-loc": newBakery("as1-loc", locator), "as2-loc": newBakery("as2-loc", locator), } ts := bakeries["ts-loc"] tsMacaroon, err := ts.Oven.NewMacaroon(testContext, bakery.LatestVersion, nil, basicOp) c.Assert(err, qt.IsNil) err = ts.Oven.AddCaveat(testContext, tsMacaroon, checkers.Caveat{Location: "as1-loc", Condition: "something"}) c.Assert(err, qt.IsNil) checker := func(loc string) bakery.ThirdPartyCaveatCheckerFunc { return func(_ context.Context, cavInfo *bakery.ThirdPartyCaveatInfo) ([]checkers.Caveat, error) { switch loc { case "as1-loc": return []checkers.Caveat{{ Condition: "something", Location: "as2-loc", }}, nil case "as2-loc": return nil, nil default: return nil, errgo.Newf("unknown location %q", loc) } } } d, err := bakery.DischargeAll(testContext, tsMacaroon, func(ctx context.Context, cav macaroon.Caveat, payload []byte) (*bakery.Macaroon, error) { return discharge(ctx, bakeries[cav.Location].Oven, checker(cav.Location), cav, payload) }) c.Assert(err, qt.IsNil) _, err = ts.Checker.Auth(d).Allow(testContext, basicOp) c.Assert(err, qt.IsNil) for i, m := range d { for j, cav := range m.Caveats() { if cav.VerificationId != nil && len(cav.Id) > 3 { c.Errorf("caveat id on caveat %d of macaroon %d is too big (%q)", j, i, cav.Id) } } } } macaroon-bakery-3.0.2/bakery/dischargeall.go000066400000000000000000000037061464427415500210360ustar00rootroot00000000000000package bakery import ( "context" "gopkg.in/errgo.v1" "gopkg.in/macaroon.v2" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/checkers" ) // DischargeAll gathers discharge macaroons for all the third party // caveats in m (and any subsequent caveats required by those) using // getDischarge to acquire each discharge macaroon. It returns a slice // with m as the first element, followed by all the discharge macaroons. // All the discharge macaroons will be bound to the primary macaroon. // // The getDischarge function is passed the caveat to be discharged; // encryptedCaveat will be passed the external caveat payload found // in m, if any. func DischargeAll( ctx context.Context, m *Macaroon, getDischarge func(ctx context.Context, cav macaroon.Caveat, encryptedCaveat []byte) (*Macaroon, error), ) (macaroon.Slice, error) { return DischargeAllWithKey(ctx, m, getDischarge, nil) } // DischargeAllWithKey is like DischargeAll except that the localKey // parameter may optionally hold the key of the client, in which case it // will be used to discharge any third party caveats with the special // location "local". In this case, the caveat itself must be "true". This // can be used be a server to ask a client to prove ownership of the // private key. // // When localKey is nil, DischargeAllWithKey is exactly the same as // DischargeAll. func DischargeAllWithKey( ctx context.Context, m *Macaroon, getDischarge func(ctx context.Context, cav macaroon.Caveat, encodedCaveat []byte) (*Macaroon, error), localKey *KeyPair, ) (macaroon.Slice, error) { discharges, err := Slice{m}.DischargeAll(ctx, getDischarge, localKey) if err != nil { return nil, errgo.Mask(err, errgo.Any) } return discharges.Bind(), nil } var localDischargeChecker = ThirdPartyCaveatCheckerFunc(func(_ context.Context, info *ThirdPartyCaveatInfo) ([]checkers.Caveat, error) { if string(info.Condition) != "true" { return nil, checkers.ErrCaveatNotRecognized } return nil, nil }) macaroon-bakery-3.0.2/bakery/dischargeall_test.go000066400000000000000000000116111464427415500220670ustar00rootroot00000000000000package bakery_test import ( "context" "fmt" "testing" qt "github.com/frankban/quicktest" "gopkg.in/errgo.v1" "gopkg.in/macaroon.v2" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/checkers" ) func alwaysOK(string) error { return nil } func TestDischargeAllNoDischarges(t *testing.T) { c := qt.New(t) rootKey := []byte("root key") m, err := bakery.NewMacaroon(rootKey, []byte("id0"), "loc0", bakery.LatestVersion, testChecker.Namespace()) c.Assert(err, qt.IsNil) ms, err := bakery.DischargeAll(testContext, m, noDischarge(c)) c.Assert(err, qt.IsNil) c.Assert(ms, qt.HasLen, 1) c.Assert(ms[0].Signature(), qt.DeepEquals, m.M().Signature()) err = m.M().Verify(rootKey, alwaysOK, nil) c.Assert(err, qt.IsNil) } func TestDischargeAllManyDischarges(t *testing.T) { c := qt.New(t) rootKey := []byte("root key") m0, err := bakery.NewMacaroon(rootKey, []byte("id0"), "loc0", bakery.LatestVersion, nil) c.Assert(err, qt.IsNil) totalRequired := 40 id := 1 addCaveats := func(m *bakery.Macaroon) { for i := 0; i < 2; i++ { if totalRequired == 0 { break } cid := fmt.Sprint("id", id) err := m.M().AddThirdPartyCaveat([]byte("root key "+cid), []byte(cid), "somewhere") c.Assert(err, qt.IsNil) id++ totalRequired-- } } addCaveats(m0) getDischarge := func(_ context.Context, cav macaroon.Caveat, payload []byte) (*bakery.Macaroon, error) { c.Check(payload, qt.IsNil) m, err := bakery.NewMacaroon([]byte("root key "+string(cav.Id)), cav.Id, "", bakery.LatestVersion, nil) c.Assert(err, qt.IsNil) addCaveats(m) return m, nil } ms, err := bakery.DischargeAll(testContext, m0, getDischarge) c.Assert(err, qt.IsNil) c.Assert(ms, qt.HasLen, 41) err = ms[0].Verify(rootKey, alwaysOK, ms[1:]) c.Assert(err, qt.IsNil) } func TestDischargeAllManyDischargesWithRealThirdPartyCaveats(t *testing.T) { c := qt.New(t) // This is the same flow as TestDischargeAllManyDischarges except that we're // using actual third party caveats as added by Macaroon.AddCaveat and // we use a larger number of caveats so that caveat ids will need to get larger. locator := bakery.NewThirdPartyStore() bakeries := make(map[string]*bakery.Bakery) bakeryId := 0 addBakery := func() string { bakeryId++ loc := fmt.Sprint("loc", bakeryId) bakeries[loc] = newBakery(loc, locator) return loc } ts := newBakery("ts-loc", locator) const totalDischargesRequired = 40 stillRequired := totalDischargesRequired checker := func(_ context.Context, ci *bakery.ThirdPartyCaveatInfo) (caveats []checkers.Caveat, _ error) { if string(ci.Condition) != "something" { return nil, errgo.Newf("unexpected condition") } for i := 0; i < 3; i++ { if stillRequired <= 0 { break } caveats = append(caveats, checkers.Caveat{ Location: addBakery(), Condition: "something", }) stillRequired-- } return caveats, nil } rootKey := []byte("root key") m0, err := bakery.NewMacaroon(rootKey, []byte("id0"), "ts-loc", bakery.LatestVersion, nil) c.Assert(err, qt.IsNil) err = m0.AddCaveat(testContext, checkers.Caveat{ Location: addBakery(), Condition: "something", }, ts.Oven.Key(), locator) c.Assert(err, qt.IsNil) // We've added a caveat (the first) so one less caveat is required. stillRequired-- getDischarge := func(ctx context.Context, cav macaroon.Caveat, payload []byte) (*bakery.Macaroon, error) { return bakery.Discharge(ctx, bakery.DischargeParams{ Id: cav.Id, Caveat: payload, Key: bakeries[cav.Location].Oven.Key(), Checker: bakery.ThirdPartyCaveatCheckerFunc(checker), Locator: locator, }) } ms, err := bakery.DischargeAll(testContext, m0, getDischarge) c.Assert(err, qt.IsNil) c.Assert(ms, qt.HasLen, totalDischargesRequired+1) err = ms[0].Verify(rootKey, alwaysOK, ms[1:]) c.Assert(err, qt.IsNil) } func TestDischargeAllLocalDischarge(t *testing.T) { c := qt.New(t) oc := newBakery("ts", nil) clientKey, err := bakery.GenerateKey() c.Assert(err, qt.IsNil) m, err := oc.Oven.NewMacaroon(testContext, bakery.LatestVersion, []checkers.Caveat{ bakery.LocalThirdPartyCaveat(&clientKey.Public, bakery.LatestVersion), }, basicOp) c.Assert(err, qt.IsNil) ms, err := bakery.DischargeAllWithKey(testContext, m, noDischarge(c), clientKey) c.Assert(err, qt.IsNil) _, err = oc.Checker.Auth(ms).Allow(testContext, basicOp) c.Assert(err, qt.IsNil) } func TestDischargeAllLocalDischargeVersion1(t *testing.T) { c := qt.New(t) oc := newBakery("ts", nil) clientKey, err := bakery.GenerateKey() c.Assert(err, qt.IsNil) m, err := oc.Oven.NewMacaroon(testContext, bakery.Version1, []checkers.Caveat{ bakery.LocalThirdPartyCaveat(&clientKey.Public, bakery.Version1), }, basicOp) c.Assert(err, qt.IsNil) ms, err := bakery.DischargeAllWithKey(testContext, m, noDischarge(c), clientKey) c.Assert(err, qt.IsNil) _, err = oc.Checker.Auth(ms).Allow(testContext, basicOp) c.Assert(err, qt.IsNil) } macaroon-bakery-3.0.2/bakery/doc.go000066400000000000000000000071471464427415500171640ustar00rootroot00000000000000// The bakery package layers on top of the macaroon package, providing // a transport and store-agnostic way of using macaroons to assert // client capabilities. // // Summary // // The Bakery type is probably where you want to start. // It encapsulates a Checker type, which performs checking // of operations, and an Oven type, which encapsulates // the actual details of the macaroon encoding conventions. // // Most other types and functions are designed either to plug // into one of the above types (the various Authorizer // implementations, for example), or to expose some independent // functionality that's potentially useful (Discharge, for example). // // The rest of this introduction introduces some of the concepts // used by the bakery package. // // Identity and entities // // An Identity represents some authenticated user (or agent), usually // the client in a network protocol. An identity can be authenticated by // an external identity server (with a third party macaroon caveat) or // by locally provided information such as a username and password. // // The Checker type is not responsible for determining identity - that // functionality is represented by the IdentityClient interface. // // The Checker uses identities to decide whether something should be // allowed or not - the Authorizer interface is used to ask whether a // given identity should be allowed to perform some set of operations. // // Operations // // An operation defines some requested action on an entity. For example, // if file system server defines an entity for every file in the server, // an operation to read a file might look like: // // Op{ // Entity: "/foo", // Action: "write", // } // // The exact set of entities and actions is up to the caller, but should // be kept stable over time because authorization tokens will contain // these names. // // To authorize some request on behalf of a remote user, first find out // what operations that request needs to perform. For example, if the // user tries to delete a file, the entity might be the path to the // file's directory and the action might be "write". It may often be // possible to determine the operations required by a request without // reference to anything external, when the request itself contains all // the necessary information. // // The LoginOp operation is special - any macaroon associated with this // operation is treated as a bearer of identity information. If two // valid LoginOp macaroons are presented, only the first one will be // used for identity. // // Authorization // // The Authorizer interface is responsible for determining whether a // given authenticated identity is authorized to perform a set of // operations. This is used when the macaroons provided to Auth are not // sufficient to authorize the operations themselves. // // Capabilities // // A "capability" is represented by a macaroon that's associated with // one or more operations, and grants the capability to perform all // those operations. The AllowCapability method reports whether a // capability is allowed. It takes into account any authenticated // identity and any other capabilities provided. // // Third party caveats // // Sometimes authorization will only be granted if a third party caveat // is discharged. This will happen when an IdentityClient or Authorizer // returns a third party caveat. // // When this happens, a DischargeRequiredError will be returned // containing the caveats and the operations required. The caller is // responsible for creating a macaroon with those caveats associated // with those operations and for passing that macaroon to the client to // discharge. package bakery macaroon-bakery-3.0.2/bakery/error.go000066400000000000000000000044631464427415500175460ustar00rootroot00000000000000package bakery import ( "fmt" "gopkg.in/errgo.v1" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/checkers" ) var ( // ErrNotFound is returned by Store.Get implementations // to signal that an id has not been found. ErrNotFound = errgo.New("not found") // ErrPermissionDenied is returned from AuthChecker when // permission has been denied. ErrPermissionDenied = errgo.New("permission denied") ) // DischargeRequiredError is returned when authorization has failed and a // discharged macaroon might fix it. // // A caller should grant the user the ability to authorize by minting a // macaroon associated with Ops (see MacaroonStore.MacaroonIdInfo for // how the associated operations are retrieved) and adding Caveats. If // the user succeeds in discharging the caveats, the authorization will // be granted. type DischargeRequiredError struct { // Message holds some reason why the authorization was denied. // TODO this is insufficient (and maybe unnecessary) because we // can have multiple errors. Message string // Ops holds all the operations that were not authorized. // If Ops contains a single LoginOp member, the macaroon // should be treated as an login token. Login tokens (also // known as authentication macaroons) usually have a longer // life span than other macaroons. Ops []Op // Caveats holds the caveats that must be added // to macaroons that authorize the above operations. Caveats []checkers.Caveat // ForAuthentication holds whether the macaroon holding // the discharges will be used for authentication, and hence // should have wider scope and longer lifetime. // The bakery package never sets this field, but bakery/identchecker // uses it. ForAuthentication bool } func (e *DischargeRequiredError) Error() string { return "macaroon discharge required: " + e.Message } func IsDischargeRequiredError(err error) bool { _, ok := err.(*DischargeRequiredError) return ok } // VerificationError is used to signify that an error is because // of a verification failure rather than because verification // could not be done. type VerificationError struct { Reason error } func (e *VerificationError) Error() string { return fmt.Sprintf("verification failed: %v", e.Reason) } func isVerificationError(err error) bool { _, ok := errgo.Cause(err).(*VerificationError) return ok } macaroon-bakery-3.0.2/bakery/example/000077500000000000000000000000001464427415500175125ustar00rootroot00000000000000macaroon-bakery-3.0.2/bakery/example/authservice.go000066400000000000000000000026371464427415500223730ustar00rootroot00000000000000package main import ( "context" "net/http" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/checkers" "github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery" ) // authService implements an authorization service, // that can discharge third-party caveats added // to other macaroons. func authService(endpoint string, key *bakery.KeyPair) (http.Handler, error) { d := httpbakery.NewDischarger(httpbakery.DischargerParams{ Checker: httpbakery.ThirdPartyCaveatCheckerFunc(thirdPartyChecker), Key: bakery.MustGenerateKey(), }) mux := http.NewServeMux() d.AddMuxHandlers(mux, "/") return mux, nil } // thirdPartyChecker is used to check third party caveats added by other // services. The HTTP request is that of the client - it is attempting // to gather a discharge macaroon. // // Note how this function can return additional first- and third-party // caveats which will be added to the original macaroon's caveats. func thirdPartyChecker(ctx context.Context, req *http.Request, info *bakery.ThirdPartyCaveatInfo, token *httpbakery.DischargeToken) ([]checkers.Caveat, error) { if string(info.Condition) != "access-allowed" { return nil, checkers.ErrCaveatNotRecognized } // TODO check that the HTTP request has cookies that prove // something about the client. return []checkers.Caveat{ httpbakery.SameClientIPAddrCaveat(req), }, nil } macaroon-bakery-3.0.2/bakery/example/client.go000066400000000000000000000017651464427415500213300ustar00rootroot00000000000000package main import ( "fmt" "io/ioutil" "net/http" "gopkg.in/errgo.v1" "github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery" ) // client represents a client of the target service. // In this simple example, it just tries a GET // request, which will fail unless the client // has the required authorization. func clientRequest(client *httpbakery.Client, serverEndpoint string) (string, error) { // The Do function implements the mechanics // of actually gathering discharge macaroons // when required, and retrying the request // when necessary. req, err := http.NewRequest("GET", serverEndpoint, nil) if err != nil { return "", errgo.Notef(err, "cannot make new HTTP request") } resp, err := client.Do(req) if err != nil { return "", errgo.NoteMask(err, "GET failed", errgo.Any) } defer resp.Body.Close() // TODO(rog) unmarshal error data, err := ioutil.ReadAll(resp.Body) if err != nil { return "", fmt.Errorf("cannot read response: %v", err) } return string(data), nil } macaroon-bakery-3.0.2/bakery/example/example_test.go000066400000000000000000000030211464427415500225270ustar00rootroot00000000000000package main import ( "net/http" "testing" qt "github.com/frankban/quicktest" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" ) func TestExample(t *testing.T) { c := qt.New(t) f := newFixture(c) client := newClient() serverEndpoint, err := serve(func(endpoint string) (http.Handler, error) { return targetService(endpoint, f.authEndpoint, f.authPublicKey) }) c.Assert(err, qt.IsNil) c.Logf("gold request") resp, err := clientRequest(client, serverEndpoint+"/gold") c.Assert(err, qt.IsNil) c.Assert(resp, qt.Equals, "all is golden") c.Logf("silver request") resp, err = clientRequest(client, serverEndpoint+"/silver") c.Assert(err, qt.IsNil) c.Assert(resp, qt.Equals, "every cloud has a silver lining") } func BenchmarkExample(b *testing.B) { c := qt.New(b) f := newFixture(c) client := newClient() serverEndpoint, err := serve(func(endpoint string) (http.Handler, error) { return targetService(endpoint, f.authEndpoint, f.authPublicKey) }) c.Assert(err, qt.IsNil) b.ResetTimer() for i := 0; i < b.N; i++ { resp, err := clientRequest(client, serverEndpoint+"/gold") c.Assert(err, qt.IsNil) c.Assert(resp, qt.Equals, "all is golden") } } type fixture struct { authEndpoint string authPublicKey *bakery.PublicKey } func newFixture(c *qt.C) *fixture { var f fixture key := bakery.MustGenerateKey() f.authPublicKey = &key.Public var err error f.authEndpoint, err = serve(func(endpoint string) (http.Handler, error) { return authService(endpoint, key) }) c.Assert(err, qt.IsNil) return &f } macaroon-bakery-3.0.2/bakery/example/main.go000066400000000000000000000044661464427415500207770ustar00rootroot00000000000000// This example demonstrates three components: // // - A target service, representing a web server that // wishes to use macaroons for authorization. // It delegates authorization to a third-party // authorization server by adding third-party // caveats to macaroons that it sends to the user. // // - A client, representing a client wanting to make // requests to the server. // // - An authorization server. // // In a real system, these three components would // live on different machines; the client component // could also be a web browser. // (TODO: write javascript discharge gatherer) package main import ( "fmt" "log" "net" "net/http" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" "github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery" ) var defaultHTTPClient = httpbakery.NewHTTPClient() func main() { key, err := bakery.GenerateKey() if err != nil { log.Fatalf("cannot generate auth service key pair: %v", err) } authPublicKey := &key.Public authEndpoint := mustServe(func(endpoint string) (http.Handler, error) { return authService(endpoint, key) }) serverEndpoint := mustServe(func(endpoint string) (http.Handler, error) { return targetService(endpoint, authEndpoint, authPublicKey) }) resp, err := clientRequest(newClient(), serverEndpoint+"/gold/") if err != nil { log.Fatalf("client failed: %v", err) } fmt.Printf("client success: %q\n", resp) resp, err = clientRequest(newClient(), serverEndpoint+"/silver/") if err != nil { log.Fatalf("client failed: %v", err) } fmt.Printf("client success: %q\n", resp) } func mustServe(newHandler func(string) (http.Handler, error)) (endpointURL string) { endpoint, err := serve(newHandler) if err != nil { log.Fatalf("cannot serve: %v", err) } return endpoint } func serve(newHandler func(string) (http.Handler, error)) (endpointURL string, err error) { listener, err := net.Listen("tcp", "localhost:0") if err != nil { return "", fmt.Errorf("cannot listen: %v", err) } endpointURL = "http://" + listener.Addr().String() handler, err := newHandler(endpointURL) if err != nil { return "", fmt.Errorf("cannot start handler: %v", err) } go http.Serve(listener, handler) return endpointURL, nil } func newClient() *httpbakery.Client { c := httpbakery.NewClient() c.AddInteractor(httpbakery.WebBrowserInteractor{}) return c } macaroon-bakery-3.0.2/bakery/example/targetservice.go000066400000000000000000000067021464427415500227150ustar00rootroot00000000000000package main import ( "context" "fmt" "net/http" "strings" "gopkg.in/errgo.v1" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/checkers" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/identchecker" "github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery" ) type targetServiceHandler struct { checker *identchecker.Checker oven *httpbakery.Oven authEndpoint string endpoint string mux *http.ServeMux } // targetService implements a "target service", representing // an arbitrary web service that wants to delegate authorization // to third parties. // func targetService(endpoint, authEndpoint string, authPK *bakery.PublicKey) (http.Handler, error) { key, err := bakery.GenerateKey() if err != nil { return nil, err } pkLocator := httpbakery.NewThirdPartyLocator(nil, nil) pkLocator.AllowInsecure() b := identchecker.NewBakery(identchecker.BakeryParams{ Key: key, Location: endpoint, Locator: pkLocator, Checker: httpbakery.NewChecker(), Authorizer: authorizer{ thirdPartyLocation: authEndpoint, }, }) mux := http.NewServeMux() srv := &targetServiceHandler{ checker: b.Checker, oven: &httpbakery.Oven{Oven: b.Oven}, authEndpoint: authEndpoint, } mux.Handle("/gold/", srv.auth(http.HandlerFunc(srv.serveGold))) mux.Handle("/silver/", srv.auth(http.HandlerFunc(srv.serveSilver))) return mux, nil } func (srv *targetServiceHandler) serveGold(w http.ResponseWriter, req *http.Request) { fmt.Fprintf(w, "all is golden") } func (srv *targetServiceHandler) serveSilver(w http.ResponseWriter, req *http.Request) { fmt.Fprintf(w, "every cloud has a silver lining") } // auth wraps the given handler with a handler that provides // authorization by inspecting the HTTP request // to decide what authorization is required. func (srv *targetServiceHandler) auth(h http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { ctx := httpbakery.ContextWithRequest(context.TODO(), req) ops, err := opsForRequest(req) if err != nil { fail(w, http.StatusInternalServerError, "%v", err) return } authChecker := srv.checker.Auth(httpbakery.RequestMacaroons(req)...) if _, err = authChecker.Allow(ctx, ops...); err != nil { httpbakery.WriteError(ctx, w, srv.oven.Error(ctx, req, err)) return } h.ServeHTTP(w, req) }) } // opsForRequest returns the required operations // implied by the given HTTP request. func opsForRequest(req *http.Request) ([]bakery.Op, error) { if !strings.HasPrefix(req.URL.Path, "/") { return nil, errgo.Newf("bad path") } elems := strings.Split(req.URL.Path, "/") if len(elems) < 2 { return nil, errgo.Newf("bad path") } return []bakery.Op{{ Entity: elems[1], Action: req.Method, }}, nil } func fail(w http.ResponseWriter, code int, msg string, args ...interface{}) { http.Error(w, fmt.Sprintf(msg, args...), code) } type authorizer struct { thirdPartyLocation string } // Authorize implements bakery.Authorizer.Authorize by // allowing anyone to do anything if a third party // approves it. func (a authorizer) Authorize(ctx context.Context, id identchecker.Identity, ops []bakery.Op) (allowed []bool, caveats []checkers.Caveat, err error) { allowed = make([]bool, len(ops)) for i := range allowed { allowed[i] = true } caveats = []checkers.Caveat{{ Location: a.thirdPartyLocation, Condition: "access-allowed", }} return } macaroon-bakery-3.0.2/bakery/export_test.go000066400000000000000000000004021464427415500207620ustar00rootroot00000000000000package bakery func SetMacaroonCaveatIdPrefix(m *Macaroon, prefix []byte) { m.caveatIdPrefix = prefix } func MacaroonCaveatData(m *Macaroon) map[string][]byte { return m.caveatData } var LegacyNamespace = legacyNamespace type MacaroonJSON macaroonJSON macaroon-bakery-3.0.2/bakery/identchecker/000077500000000000000000000000001464427415500205075ustar00rootroot00000000000000macaroon-bakery-3.0.2/bakery/identchecker/authorizer.go000066400000000000000000000121031464427415500232270ustar00rootroot00000000000000package identchecker import ( "context" "gopkg.in/errgo.v1" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/checkers" ) // Authorizer is used to check whether a given user is allowed // to perform a set of operations. type Authorizer interface { // Authorize checks whether the given identity (which will be nil // when there is no authenticated user) is allowed to perform // the given operations. It should return an error only when // the authorization cannot be determined, not when the // user has been denied access. // // On success, each element of allowed holds whether the respective // element of ops has been allowed, and caveats holds any additional // third party caveats that apply. // If allowed is shorter then ops, the additional elements are assumed to // be false. Authorize(ctx context.Context, id Identity, ops []bakery.Op) (allowed []bool, caveats []checkers.Caveat, err error) } var ( // OpenAuthorizer is an Authorizer implementation that will authorize all operations without question. OpenAuthorizer openAuthorizer // ClosedAuthorizer is an Authorizer implementation that will return ErrPermissionDenied // on all authorization requests. ClosedAuthorizer closedAuthorizer ) var ( _ Authorizer = OpenAuthorizer _ Authorizer = ClosedAuthorizer _ Authorizer = AuthorizerFunc(nil) _ Authorizer = ACLAuthorizer{} ) type openAuthorizer struct{} // Authorize implements Authorizer.Authorize. func (openAuthorizer) Authorize(ctx context.Context, id Identity, ops []bakery.Op) (allowed []bool, caveats []checkers.Caveat, err error) { allowed = make([]bool, len(ops)) for i := range allowed { allowed[i] = true } return allowed, nil, nil } type closedAuthorizer struct{} // Authorize implements Authorizer.Authorize. func (closedAuthorizer) Authorize(ctx context.Context, id Identity, ops []bakery.Op) (allowed []bool, caveats []checkers.Caveat, err error) { return make([]bool, len(ops)), nil, nil } // AuthorizerFunc implements a simplified version of Authorizer // that operates on a single operation at a time. type AuthorizerFunc func(ctx context.Context, id Identity, op bakery.Op) (bool, []checkers.Caveat, error) // Authorize implements Authorizer.Authorize by calling f // with the given identity for each operation. func (f AuthorizerFunc) Authorize(ctx context.Context, id Identity, ops []bakery.Op) (allowed []bool, caveats []checkers.Caveat, err error) { allowed = make([]bool, len(ops)) for i, op := range ops { ok, fcaveats, err := f(ctx, id, op) if err != nil { return nil, nil, errgo.Mask(err) } allowed[i] = ok // TODO merge identical caveats? caveats = append(caveats, fcaveats...) } return allowed, caveats, nil } // Everyone is recognized by ACLAuthorizer as the name of a // group that has everyone in it. const Everyone = "everyone" // ACLIdentity may be implemented by Identity implementions // to report group membership information. // See ACLAuthorizer for details. type ACLIdentity interface { Identity // Allow reports whether the user should be allowed to access // any of the users or groups in the given ACL slice. Allow(ctx context.Context, acl []string) (bool, error) } // ACLAuthorizer is an Authorizer implementation that will check access // control list (ACL) membership of users. It uses GetACL to find out // the ACLs that apply to the requested operations and will authorize an // operation if an ACL contains the group "everyone" or if the context // contains an AuthInfo (see ContextWithAuthInfo) that holds an Identity // that implements ACLIdentity and its Allow method returns true for the // ACL. type ACLAuthorizer struct { // GetACL returns the ACL that applies to the given operation, // and reports whether non-authenticated users should // be allowed access when the ACL contains "everyone". // // If an entity cannot be found or the action is not recognised, // GetACLs should return an empty ACL but no error. GetACL func(ctx context.Context, op bakery.Op) (acl []string, allowPublic bool, err error) } // Authorize implements Authorizer.Authorize by calling ident.Allow to determine // whether the identity is a member of the ACLs associated with the given // operations. func (a ACLAuthorizer) Authorize(ctx context.Context, ident Identity, ops []bakery.Op) (allowed []bool, caveats []checkers.Caveat, err error) { if len(ops) == 0 { // Anyone is allowed to do nothing. return nil, nil, nil } ident1, _ := ident.(ACLIdentity) allowed = make([]bool, len(ops)) for i, op := range ops { acl, allowPublic, err := a.GetACL(ctx, op) if err != nil { return nil, nil, errgo.Mask(err) } if ident1 != nil { allowed[i], err = ident1.Allow(ctx, acl) if err != nil { return nil, nil, errgo.Notef(err, "cannot check permissions") } } else { // TODO should we allow "everyone" when the identity is // non-nil but isn't an ACLIdentity? allowed[i] = allowPublic && isPublicACL(acl) } } return allowed, nil, nil } func isPublicACL(acl []string) bool { for _, g := range acl { if g == Everyone { return true } } return false } macaroon-bakery-3.0.2/bakery/identchecker/authorizer_test.go000066400000000000000000000112571464427415500242770ustar00rootroot00000000000000package identchecker_test import ( "context" "testing" qt "github.com/frankban/quicktest" "gopkg.in/errgo.v1" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/checkers" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/identchecker" ) func TestAuthorizerFunc(t *testing.T) { c := qt.New(t) f := func(ctx context.Context, id identchecker.Identity, op bakery.Op) (bool, []checkers.Caveat, error) { c.Assert(ctx, qt.Equals, testContext) c.Assert(id, qt.Equals, identchecker.SimpleIdentity("bob")) switch op.Entity { case "a": return false, nil, nil case "b": return true, nil, nil case "c": return true, []checkers.Caveat{{ Location: "somewhere", Condition: "c", }}, nil case "d": return true, []checkers.Caveat{{ Location: "somewhere", Condition: "d", }}, nil } c.Fatalf("unexpected entity: %q", op.Entity) return false, nil, nil } allowed, caveats, err := identchecker.AuthorizerFunc(f).Authorize(testContext, identchecker.SimpleIdentity("bob"), []bakery.Op{{"a", "x"}, {"b", "x"}, {"c", "x"}, {"d", "x"}}) c.Assert(err, qt.IsNil) c.Assert(allowed, qt.DeepEquals, []bool{false, true, true, true}) c.Assert(caveats, qt.DeepEquals, []checkers.Caveat{{ Location: "somewhere", Condition: "c", }, { Location: "somewhere", Condition: "d", }}) } var aclAuthorizerTests = []struct { about string auth identchecker.ACLAuthorizer identity identchecker.Identity ops []bakery.Op expectAllowed []bool expectError string }{{ about: "no ops, no problem", auth: identchecker.ACLAuthorizer{ GetACL: func(ctx context.Context, op bakery.Op) ([]string, bool, error) { return nil, false, nil }, }, }, { about: "identity that does not implement ACLIdentity; user should be denied except for everyone group", auth: identchecker.ACLAuthorizer{ GetACL: func(ctx context.Context, op bakery.Op) ([]string, bool, error) { if op.Entity == "a" { return []string{identchecker.Everyone}, true, nil } else { return []string{"alice"}, false, nil } }, }, identity: simplestIdentity("bob"), ops: []bakery.Op{{ Entity: "a", Action: "a", }, { Entity: "b", Action: "b", }}, expectAllowed: []bool{true, false}, }, { about: "identity that does not implement ACLIdentity with user == Id; user should be denied except for everyone group", auth: identchecker.ACLAuthorizer{ GetACL: func(ctx context.Context, op bakery.Op) ([]string, bool, error) { if op.Entity == "a" { return []string{identchecker.Everyone}, true, nil } else { return []string{"bob"}, false, nil } }, }, identity: simplestIdentity("bob"), ops: []bakery.Op{{ Entity: "a", Action: "a", }, { Entity: "b", Action: "b", }}, expectAllowed: []bool{true, false}, }, { about: "permission denied for everyone without allow-public", auth: identchecker.ACLAuthorizer{ GetACL: func(ctx context.Context, op bakery.Op) ([]string, bool, error) { return []string{identchecker.Everyone}, false, nil }, }, identity: simplestIdentity("bob"), ops: []bakery.Op{{ Entity: "a", Action: "a", }}, expectAllowed: []bool{false}, }, { about: "permission granted to anyone with no identity with allow-public", auth: identchecker.ACLAuthorizer{ GetACL: func(ctx context.Context, op bakery.Op) ([]string, bool, error) { return []string{identchecker.Everyone}, true, nil }, }, ops: []bakery.Op{{ Entity: "a", Action: "a", }}, expectAllowed: []bool{true}, }, { about: "error return causes all authorization to fail", auth: identchecker.ACLAuthorizer{ GetACL: func(ctx context.Context, op bakery.Op) ([]string, bool, error) { if op.Entity == "a" { return []string{identchecker.Everyone}, true, nil } else { return nil, false, errgo.New("some error") } }, }, ops: []bakery.Op{{ Entity: "a", Action: "a", }, { Entity: "b", Action: "b", }}, expectError: "some error", }} func TestACLAuthorizer(t *testing.T) { c := qt.New(t) for i, test := range aclAuthorizerTests { c.Logf("test %d: %v", i, test.about) allowed, caveats, err := test.auth.Authorize(context.Background(), test.identity, test.ops) if test.expectError != "" { c.Assert(err, qt.ErrorMatches, test.expectError) c.Assert(allowed, qt.IsNil) c.Assert(caveats, qt.IsNil) continue } c.Assert(err, qt.IsNil) c.Assert(caveats, qt.IsNil) c.Assert(allowed, qt.DeepEquals, test.expectAllowed) } } // simplestIdentity implements Identity for a string. Unlike // simpleIdentity, it does not implement ACLIdentity. type simplestIdentity string func (id simplestIdentity) Id() string { return string(id) } func (simplestIdentity) Domain() string { return "" } macaroon-bakery-3.0.2/bakery/identchecker/bakery.go000066400000000000000000000060211464427415500223120ustar00rootroot00000000000000package identchecker import ( "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/checkers" ) type Bakery struct { Oven *bakery.Oven Checker *Checker } // BakeryParams holds a selection of parameters for the Oven // and the Checker created by New. // // For more fine-grained control of parameters, create the // Oven or Checker directly. // // The zero value is OK to use, but won't allow any authentication // or third party caveats to be added. type BakeryParams struct { // Checker holds the checker used to check first party caveats. // If this is nil, New will use checkers.New(nil). Checker bakery.FirstPartyCaveatChecker // RootKeyStore holds the root key store to use. If you need to // use a different root key store for different operations, // you'll need to pass a RootKeyStoreForOps value to NewOven // directly. // // If this is nil, New will use NewMemRootKeyStore(). // Note that that is almost certain insufficient for production services // that are spread across multiple instances or that need // to persist keys across restarts. RootKeyStore bakery.RootKeyStore // Locator is used to find out information on third parties when // adding third party caveats. If this is nil, no non-local third // party caveats can be added. Locator bakery.ThirdPartyLocator // Key holds the private key of the oven. If this is nil, // no third party caveats may be added. Key *bakery.KeyPair // IdentityClient holds the identity implementation to use for // authentication. If this is nil, no authentication will be possible. IdentityClient IdentityClient // Authorizer is used to check whether an authenticated user is // allowed to perform operations. If it is nil, New will // use ClosedAuthorizer. // // The identity parameter passed to Authorizer.Allow will // always have been obtained from a call to // IdentityClient.DeclaredIdentity. Authorizer Authorizer // Location holds the location to use when creating new macaroons. Location string // Logger is used to log checker operations. If it is nil, // DefaultLogger("bakery.identchecker") will be used. Logger bakery.Logger } // NewBakery returns a new Bakery instance which combines an Oven with a // Checker for the convenience of callers that wish to use both // together. func NewBakery(p BakeryParams) *Bakery { if p.Checker == nil { p.Checker = checkers.New(nil) } ovenParams := bakery.OvenParams{ Key: p.Key, Namespace: p.Checker.Namespace(), Location: p.Location, Locator: p.Locator, LegacyMacaroonOp: LoginOp, } if p.RootKeyStore != nil { ovenParams.RootKeyStoreForOps = func(ops []bakery.Op) bakery.RootKeyStore { return p.RootKeyStore } } oven := bakery.NewOven(ovenParams) checker := NewChecker(CheckerParams{ Checker: p.Checker, MacaroonVerifier: oven, IdentityClient: p.IdentityClient, Authorizer: p.Authorizer, Logger: p.Logger, }) return &Bakery{ Oven: oven, Checker: checker, } } macaroon-bakery-3.0.2/bakery/identchecker/checker.go000066400000000000000000000247761464427415500224620ustar00rootroot00000000000000// Package identchecker wraps the functionality in the bakery // package to add support for authentication via third party // caveats. package identchecker import ( "context" "sync" "gopkg.in/errgo.v1" "gopkg.in/macaroon.v2" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/checkers" ) // CheckerParams holds parameters for NewChecker. // The only mandatory parameter is MacaroonVerifier. type CheckerParams struct { // MacaroonVerifier is used to retrieve macaroon root keys // and other associated information. MacaroonVerifier bakery.MacaroonVerifier // Checker is used to check first party caveats when authorizing. // If this is nil NewChecker will use checkers.New(nil). Checker bakery.FirstPartyCaveatChecker // OpsAuthorizer is used to check whether operations are authorized // by some other already-authorized operation. If it is nil, // NewChecker will assume no operation is authorized by any // operation except itself. OpsAuthorizer bakery.OpsAuthorizer // IdentityClient is used for interactions with the external // identity service used for authentication. // // If this is nil, no authentication will be possible. IdentityClient IdentityClient // Authorizer is used to check whether an authenticated user is // allowed to perform operations. If it is nil, NewChecker will // use ClosedAuthorizer. // // The identity parameter passed to Authorizer.Allow will // always have been obtained from a call to // IdentityClient.DeclaredIdentity. Authorizer Authorizer // Logger is used to log checker operations. If it is nil, // DefaultLogger("bakery.identchecker") will be used. Logger bakery.Logger } // NewChecker returns a new Checker using the given parameters. func NewChecker(p CheckerParams) *Checker { if p.IdentityClient == nil { p.IdentityClient = noIdentities{} } if p.Authorizer == nil { p.Authorizer = ClosedAuthorizer } if p.Logger == nil { p.Logger = bakery.DefaultLogger("bakery.identchecker") } c := &Checker{ p: p, } bp := bakery.CheckerParams{ Checker: p.Checker, OpsAuthorizer: identityOpsAuthorizer{c}, MacaroonVerifier: p.MacaroonVerifier, } return &Checker{ checker: bakery.NewChecker(bp), p: p, } } // Checker is similar to bakery.Checker but also knows // about authentication, and can use an authenticated identity // to authorize operations. type Checker struct { checker *bakery.Checker p CheckerParams } // Namespace returns the first-party caveat namespace // used by the checker. func (c *Checker) Namespace() *checkers.Namespace { return c.checker.Namespace() } // Auth makes a new AuthChecker instance using the // given macaroons to inform authorization decisions. // The identity is authenticated only once, the first time any method // of the AuthChecker is called, using the context passed in then. // // To find out any declared identity without requiring a login, // use Allow(ctx); to require authentication but no additional operations, // use Allow(ctx, LoginOp). func (c *Checker) Auth(mss ...macaroon.Slice) *AuthChecker { return &AuthChecker{ checker: c, authChecker: c.checker.Auth(mss...), } } // AuthInfo information about an authorization decision. type AuthInfo struct { *bakery.AuthInfo // Identity holds information on the authenticated user as returned // from IdentityClient. It may be nil after a // successful authorization if LoginOp access was not required. Identity Identity } // LoginOp represents a login (authentication) operation. // A macaroon that is associated with this operation generally // carries authentication information with it. var LoginOp = bakery.Op{ Entity: "login", Action: "login", } // AuthChecker authorizes operations with respect to a user's request. type AuthChecker struct { checker *Checker authChecker *bakery.AuthChecker // mu guards the identity_ field. mu sync.Mutex // identity_ holds the first identity discovered by AuthorizeOps // so that all authorizations use a consistent identity. identity_ Identity } // Allow checks that the authorizer's request is authorized to // perform all the given operations. // // If all the operations are allowed, an AuthInfo is returned holding // details of the decision. // // If an operation was not allowed, an error will be returned which may // be *bakery.DischargeRequiredError holding the operations that remain to // be authorized in order to allow authorization to // proceed. func (c *AuthChecker) Allow(ctx context.Context, ops ...bakery.Op) (*AuthInfo, error) { loginInfo, loginErr := c.authChecker.Allow(ctx, LoginOp) c.checker.p.Logger.Infof(ctx, "allow loginop: %#v; err %#v", loginInfo, loginErr) var identity Identity var identityCaveats []checkers.Caveat if loginErr == nil { // We've got a login macaroon. Extract the identity from it. identity1, err := c.inferIdentityFromMacaroon(ctx, c.authChecker.Namespace(), loginInfo.Macaroons[loginInfo.OpIndexes[LoginOp]]) if err != nil { return nil, errgo.Mask(err) } identity = identity1 } else { // No login macaroon found. Try to infer an identity from the context. identity1, caveats1, err := c.inferIdentityFromContext(ctx) if err != nil { return nil, errgo.WithCausef(err, bakery.ErrPermissionDenied, "") } identity, identityCaveats = identity1, caveats1 mss := c.authChecker.Macaroons() loginInfo = &bakery.AuthInfo{ Macaroons: mss, Used: make([]bool, len(mss)), OpIndexes: make(map[bakery.Op]int), } } // Form a slice holding all the non-login operations that are required. need := make([]bakery.Op, 0, len(ops)) for _, op := range ops { if op != LoginOp { need = append(need, op) } } if len(need) == 0 && identity != nil { // No operations other than LoginOp required, and we've // got an identity, so nothing more to do. return &AuthInfo{ Identity: identity, AuthInfo: loginInfo, }, nil } // Check the remaining operations only there are more to // authorize, and if we have an identity or we don't need one. needLogin := len(need) != len(ops) if len(need) > 0 && (!needLogin || identity != nil) { // Make the AuthChecker available to the OpsAuthorizer // so that it can use any identity we inferred above in // the authorization decision. ctx := contextWithIdentity(ctx, identity) opInfo, err := c.authChecker.Allow(ctx, need...) if err == nil { // All operations allowed. if loginErr == nil { for i, used := range loginInfo.Used { if used { opInfo.Used[i] = used } } opInfo.OpIndexes[LoginOp] = loginInfo.OpIndexes[LoginOp] } return &AuthInfo{ AuthInfo: opInfo, Identity: identity, }, nil } if bakery.IsDischargeRequiredError(err) { return nil, errgo.Mask(err, bakery.IsDischargeRequiredError) } } if identity != nil || len(identityCaveats) == 0 { return nil, errgo.WithCausef(loginErr, bakery.ErrPermissionDenied, "") } return nil, &bakery.DischargeRequiredError{ Message: "authentication required", Ops: []bakery.Op{LoginOp}, Caveats: identityCaveats, ForAuthentication: true, } } type identityOpsAuthorizer struct { checker *Checker } // AuthorizeOps implements bakery.OpsAuthorizer. It allows LoginOp // to authorize operations by using the Authorizer passed to // NewChecker, and falls back to OpsAuthorizer for other operations. // // Once an identity has been determined, the same identity will always // be used. func (a identityOpsAuthorizer) AuthorizeOps(ctx context.Context, authorizedOp bakery.Op, queryOps []bakery.Op) ([]bool, []checkers.Caveat, error) { identity, ok := identityFromContext(ctx) if !ok { // There's no identity associated with the context, which means we're // authorizing before we've inferred the identity, so we // can't authorize any indirect operations. return nil, nil, nil } // There might or might not be an actual LoginOp macaroon, so // we authorize from NoOp instead, which is always used last. if authorizedOp != bakery.NoOp { if a.checker.p.OpsAuthorizer != nil { return a.checker.p.OpsAuthorizer.AuthorizeOps(ctx, authorizedOp, queryOps) } return nil, nil, nil } allowed, caveats, err := a.checker.p.Authorizer.Authorize(ctx, identity, queryOps) return allowed, caveats, errgo.Mask(err) } // inferIdentityFromMacaroon ensures sure that we always use the same identity for authorization. // The op argument holds an authorized operation func (c *AuthChecker) inferIdentityFromMacaroon(ctx context.Context, ns *checkers.Namespace, ms macaroon.Slice) (Identity, error) { c.mu.Lock() defer c.mu.Unlock() if c.identity_ != nil { return c.identity_, nil } declared := checkers.InferDeclared(ns, ms) identity, err := c.checker.p.IdentityClient.DeclaredIdentity(ctx, declared) if err != nil { return nil, errgo.Notef(err, "could not determine identity") } if identity == nil { // Shouldn't happen if DeclaredIdentity behaves itself, but // be defensive just in case. return nil, errgo.Newf("no declared identity found in LoginOp macaroon") } c.identity_ = identity return identity, nil } // inferIdentity extracts an Identity from ctx, if there is one, and stores it // in c.identity_ to ensure that we always use the same identity for authorization. // It returns either the identity or a set of third party caveats that, when // discharged, will allow us to determine an identity. func (c *AuthChecker) inferIdentityFromContext(ctx context.Context) (Identity, []checkers.Caveat, error) { c.mu.Lock() defer c.mu.Unlock() if c.identity_ != nil { return c.identity_, nil, nil } // NoOp is used after all the other operations have been tried, so we've // already tried any LoginOp macaroons if present. identity, caveats, err := c.checker.p.IdentityClient.IdentityFromContext(ctx) if err != nil { return nil, nil, errgo.Notef(err, "could not determine identity") } if len(caveats) != 0 { return nil, caveats, nil } c.identity_ = identity return identity, nil, nil } type identityKey struct{} // identityVal holds an Identity in a context. We use a struct // rather than storing the identity directly so that we can know // when a context is associated with a nil identity, // so that AuthorizeOps can tell that it's being called before // any identity has been determined. type identityVal struct { Identity } func contextWithIdentity(ctx context.Context, identity Identity) context.Context { return context.WithValue(ctx, identityKey{}, identityVal{identity}) } func identityFromContext(ctx context.Context) (Identity, bool) { idVal, ok := ctx.Value(identityKey{}).(identityVal) return idVal.Identity, ok } macaroon-bakery-3.0.2/bakery/identchecker/checker_test.go000066400000000000000000000630171464427415500235100ustar00rootroot00000000000000package identchecker_test import ( "context" "sort" "strings" "testing" qt "github.com/frankban/quicktest" "gopkg.in/errgo.v1" "gopkg.in/macaroon.v2" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/checkers" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/identchecker" ) type dischargeRecord struct { Location string User string } func TestAuthorizeWithOpenAccessAndNoMacaroons(t *testing.T) { c := qt.New(t) locator := make(dischargerLocator) ids := newIdService("ids", locator) auth := opACL{readOp("something"): {identchecker.Everyone}} ts := newService(auth, ids, locator) client := newClient(locator) authInfo, err := client.do(testContext, ts, readOp("something")) c.Assert(err, qt.IsNil) c.Assert(ids.discharges, qt.HasLen, 0) c.Assert(authInfo, qt.Not(qt.IsNil)) c.Assert(authInfo.Identity, qt.Equals, nil) c.Assert(authInfo.Macaroons, qt.HasLen, 0) } func TestAuthorizationDenied(t *testing.T) { c := qt.New(t) locator := make(dischargerLocator) ids := newIdService("ids", locator) auth := identchecker.ClosedAuthorizer ts := newService(auth, ids, locator) client := newClient(locator) authInfo, err := client.do(asUser("bob"), ts, readOp("something")) c.Assert(err, qt.ErrorMatches, `permission denied`) c.Assert(authInfo, qt.IsNil) } func TestAuthorizeWithAuthenticationRequired(t *testing.T) { c := qt.New(t) locator := make(dischargerLocator) ids := newIdService("ids", locator) auth := opACL{readOp("something"): {"bob"}} ts := newService(auth, ids, locator) client := newClient(locator) authInfo, err := client.do(asUser("bob"), ts, readOp("something")) c.Assert(err, qt.IsNil) c.Assert(ids.discharges, qt.DeepEquals, []dischargeRecord{{ Location: "ids", User: "bob", }}) c.Assert(authInfo, qt.Not(qt.IsNil)) c.Assert(authInfo.Identity, qt.Equals, identchecker.SimpleIdentity("bob")) c.Assert(authInfo.Macaroons, qt.HasLen, 1) } func asUser(username string) context.Context { return contextWithDischargeUser(testContext, username) } func TestAuthorizeMultipleOps(t *testing.T) { c := qt.New(t) locator := make(dischargerLocator) ids := newIdService("ids", locator) auth := opACL{readOp("something"): {"bob"}, readOp("otherthing"): {"bob"}} ts := newService(auth, ids, locator) client := newClient(locator) _, err := client.do(asUser("bob"), ts, readOp("something"), readOp("otherthing")) c.Assert(err, qt.IsNil) c.Assert(ids.discharges, qt.DeepEquals, []dischargeRecord{{ Location: "ids", User: "bob", }}) } func TestCapability(t *testing.T) { c := qt.New(t) locator := make(dischargerLocator) ids := newIdService("ids", locator) auth := opACL{readOp("something"): {"bob"}} ts := newService(auth, ids, locator) client := newClient(locator) m, err := client.dischargedCapability(asUser("bob"), ts, readOp("something")) c.Assert(err, qt.IsNil) // Check that we can exercise the capability directly on the service // with no discharging required. authInfo, err := ts.do(testContext, []macaroon.Slice{m}, readOp("something")) c.Assert(err, qt.IsNil) c.Assert(authInfo, qt.Not(qt.IsNil)) c.Assert(authInfo.Identity, qt.Equals, nil) c.Assert(authInfo.Macaroons, qt.HasLen, 1) c.Assert(authInfo.Macaroons[0][0].Id(), qt.DeepEquals, m[0].Id()) } func TestCapabilityMultipleEntities(t *testing.T) { c := qt.New(t) locator := make(dischargerLocator) ids := newIdService("ids", locator) auth := opACL{readOp("e1"): {"bob"}, readOp("e2"): {"bob"}, readOp("e3"): {"bob"}} ts := newService(auth, ids, locator) client := newClient(locator) m, err := client.dischargedCapability(asUser("bob"), ts, readOp("e1"), readOp("e2"), readOp("e3")) c.Assert(err, qt.IsNil) c.Assert(ids.discharges, qt.DeepEquals, []dischargeRecord{{ Location: "ids", User: "bob", }}) // Check that we can exercise the capability directly on the service // with no discharging required. _, err = ts.do(testContext, []macaroon.Slice{m}, readOp("e1"), readOp("e2"), readOp("e3")) c.Assert(err, qt.IsNil) // Check that we can exercise the capability to act on a subset of the operations. _, err = ts.do(testContext, []macaroon.Slice{m}, readOp("e2"), readOp("e3")) c.Assert(err, qt.IsNil) _, err = ts.do(testContext, []macaroon.Slice{m}, readOp("e3")) c.Assert(err, qt.IsNil) } func TestMultipleCapabilities(t *testing.T) { c := qt.New(t) locator := make(dischargerLocator) ids := newIdService("ids", locator) auth := opACL{readOp("e1"): {"alice"}, readOp("e2"): {"bob"}} ts := newService(auth, ids, locator) // Acquire two capabilities as different users and check // that we can combine them together to do both operations // at once. m1, err := newClient(locator).dischargedCapability(asUser("alice"), ts, readOp("e1")) c.Assert(err, qt.IsNil) m2, err := newClient(locator).dischargedCapability(asUser("bob"), ts, readOp("e2")) c.Assert(err, qt.IsNil) c.Assert(ids.discharges, qt.DeepEquals, []dischargeRecord{{ Location: "ids", User: "alice", }, { Location: "ids", User: "bob", }}) authInfo, err := ts.do(testContext, []macaroon.Slice{m1, m2}, readOp("e1"), readOp("e2")) c.Assert(err, qt.IsNil) c.Assert(authInfo, qt.Not(qt.IsNil)) c.Assert(authInfo.Identity, qt.Equals, nil) c.Assert(authInfo.Macaroons, qt.HasLen, 2) c.Assert(authInfo.Macaroons[0][0].Id(), qt.DeepEquals, m1[0].Id()) c.Assert(authInfo.Macaroons[1][0].Id(), qt.DeepEquals, m2[0].Id()) } func TestCombineCapabilities(t *testing.T) { c := qt.New(t) locator := make(dischargerLocator) ids := newIdService("ids", locator) auth := opACL{readOp("e1"): {"alice"}, readOp("e2"): {"bob"}, readOp("e3"): {"bob", "alice"}} ts := newService(auth, ids, locator) // Acquire two capabilities as different users and check // that we can combine them together into a single capability // capable of both operations. m1, err := newClient(locator).dischargedCapability(asUser("alice"), ts, readOp("e1"), readOp("e3")) c.Assert(err, qt.IsNil) m2, err := newClient(locator).dischargedCapability(asUser("bob"), ts, readOp("e2")) c.Assert(err, qt.IsNil) m, err := ts.capability(testContext, []macaroon.Slice{m1, m2}, readOp("e1"), readOp("e2"), readOp("e3")) c.Assert(err, qt.IsNil) _, err = ts.do(testContext, []macaroon.Slice{{m.M()}}, readOp("e1"), readOp("e2"), readOp("e3")) c.Assert(err, qt.IsNil) } func TestPartiallyAuthorizedRequest(t *testing.T) { c := qt.New(t) locator := make(dischargerLocator) ids := newIdService("ids", locator) auth := opACL{readOp("e1"): {"alice"}, readOp("e2"): {"bob"}} ts := newService(auth, ids, locator) // Acquire a capability for e1 but rely on authentication to // authorize e2. m, err := newClient(locator).dischargedCapability(asUser("alice"), ts, readOp("e1")) c.Assert(err, qt.IsNil) client := newClient(locator) client.addMacaroon(ts, "authz", m) _, err = client.do(asUser("bob"), ts, readOp("e1"), readOp("e2")) c.Assert(err, qt.IsNil) } func TestAuthWithThirdPartyCaveats(t *testing.T) { c := qt.New(t) locator := make(dischargerLocator) ids := newIdService("ids", locator) // We make an authorizer that requires a third party discharge // when authorizing. auth := identchecker.AuthorizerFunc(func(_ context.Context, id identchecker.Identity, op bakery.Op) (bool, []checkers.Caveat, error) { if id == identchecker.SimpleIdentity("bob") && op == readOp("something") { return true, []checkers.Caveat{{ Condition: "question", Location: "other third party", }}, nil } return false, nil, nil }) ts := newService(auth, ids, locator) locator["other third party"] = &discharger{ key: mustGenerateKey(), checker: bakery.ThirdPartyCaveatCheckerFunc(func(ctx context.Context, info *bakery.ThirdPartyCaveatInfo) ([]checkers.Caveat, error) { if string(info.Condition) != "question" { return nil, errgo.Newf("third party condition not recognized") } ids.discharges = append(ids.discharges, dischargeRecord{ Location: "other third party", User: dischargeUserFromContext(ctx), }) return nil, nil }), locator: locator, } client := newClient(locator) _, err := client.do(asUser("bob"), ts, readOp("something")) c.Assert(err, qt.IsNil) c.Assert(ids.discharges, qt.DeepEquals, []dischargeRecord{{ Location: "ids", User: "bob", }, { Location: "other third party", User: "bob", }}) } func TestCapabilityCombinesFirstPartyCaveats(t *testing.T) { c := qt.New(t) locator := make(dischargerLocator) ids := newIdService("ids", locator) auth := opACL{readOp("e1"): {"alice"}, readOp("e2"): {"bob"}} ts := newService(auth, ids, locator) // Acquire two capabilities as different users, add some first party caveats // // that we can combine them together into a single capability // capable of both operations. m1, err := newClient(locator).capability(asUser("alice"), ts, readOp("e1")) c.Assert(err, qt.IsNil) m1.M().AddFirstPartyCaveat([]byte("true 1")) m1.M().AddFirstPartyCaveat([]byte("true 2")) m2, err := newClient(locator).capability(asUser("bob"), ts, readOp("e2")) c.Assert(err, qt.IsNil) m2.M().AddFirstPartyCaveat([]byte("true 3")) m2.M().AddFirstPartyCaveat([]byte("true 4")) client := newClient(locator) client.addMacaroon(ts, "authz1", macaroon.Slice{m1.M()}) client.addMacaroon(ts, "authz2", macaroon.Slice{m2.M()}) m, err := client.capability(testContext, ts, readOp("e1"), readOp("e2")) c.Assert(err, qt.IsNil) c.Assert(macaroonConditions(m.M().Caveats(), false), qt.DeepEquals, []string{ "true 1", "true 2", "true 3", "true 4", }) } func TestLoginOnly(t *testing.T) { c := qt.New(t) locator := make(dischargerLocator) ids := newIdService("ids", locator) auth := identchecker.ClosedAuthorizer ts := newService(auth, ids, locator) authInfo, err := newClient(locator).do(asUser("bob"), ts, identchecker.LoginOp) c.Assert(err, qt.IsNil) c.Assert(authInfo.Identity, qt.Equals, identchecker.SimpleIdentity("bob")) } func TestAuthWithIdentityFromContext(t *testing.T) { c := qt.New(t) locator := make(dischargerLocator) ids := basicAuthIdService{} auth := opACL{readOp("e1"): {"sherlock"}, readOp("e2"): {"bob"}} ts := newService(auth, ids, locator) // Check that we can perform the ops with basic auth in the // context. authInfo, err := newClient(locator).do(contextWithBasicAuth(testContext, "sherlock", "holmes"), ts, readOp("e1")) c.Assert(err, qt.IsNil) c.Assert(authInfo.Identity, qt.Equals, identchecker.SimpleIdentity("sherlock")) c.Assert(authInfo.Macaroons, qt.HasLen, 0) } func TestAuthLoginOpWithIdentityFromContext(t *testing.T) { c := qt.New(t) locator := make(dischargerLocator) ids := basicAuthIdService{} ts := newService(nil, ids, locator) // Check that we can use LoginOp when auth isn't granted through macaroons. authInfo, err := newClient(locator).do(contextWithBasicAuth(testContext, "sherlock", "holmes"), ts, identchecker.LoginOp) c.Assert(err, qt.IsNil) if err == nil && authInfo == nil { c.Fatalf("nil err and nil authInfo") } c.Assert(authInfo.Identity, qt.Equals, identchecker.SimpleIdentity("sherlock")) c.Assert(authInfo.Macaroons, qt.HasLen, 0) } func TestAllowWithOpsAuthorizer(t *testing.T) { c := qt.New(t) locator := make(dischargerLocator) oven := newMacaroonStore(mustGenerateKey(), locator) ts := &service{ // Note: we're making a checker with no Authorizer and no IdentityClient. checker: identchecker.NewChecker(identchecker.CheckerParams{ Checker: testChecker, OpsAuthorizer: hierarchicalOpsAuthorizer{}, MacaroonVerifier: oven, }), oven: oven, } // Manufacture a macaroon granting access to /user/bob and // everything underneath it (by virtue of the hierarchicalOpsAuthorizer). m, err := ts.oven.NewMacaroon(testContext, bakery.LatestVersion, nil, bakery.Op{ Entity: "path-/user/bob", Action: "*", }) c.Assert(err, qt.Equals, nil) // Check that we can do some operation. _, err = ts.do(testContext, []macaroon.Slice{{m.M()}}, writeOp("path-/user/bob/foo")) c.Assert(err, qt.Equals, nil) // Check that we can't do an operation on an entity outside the // original operation's purview. _, err = ts.do(testContext, []macaroon.Slice{{m.M()}}, writeOp("path-/user/alice")) c.Assert(err, qt.ErrorMatches, `permission denied`) } func TestDuplicateLoginMacaroons(t *testing.T) { c := qt.New(t) locator := make(dischargerLocator) ids := newIdService("ids", locator) auth := identchecker.ClosedAuthorizer ts := newService(auth, ids, locator) // Acquire a login macaroon for bob. client1 := newClient(locator) authInfo, err := client1.do(asUser("bob"), ts, identchecker.LoginOp) c.Assert(err, qt.IsNil) c.Assert(authInfo.Identity, qt.Equals, identchecker.SimpleIdentity("bob")) // Acquire a login macaroon for alice. client2 := newClient(locator) authInfo, err = client2.do(asUser("alice"), ts, identchecker.LoginOp) c.Assert(err, qt.IsNil) c.Assert(authInfo.Identity, qt.Equals, identchecker.SimpleIdentity("alice")) // Combine the two login macaroons into one client. client3 := newClient(locator) client3.addMacaroon(ts, "1.bob", client1.macaroons[ts]["authn"]) client3.addMacaroon(ts, "2.alice", client2.macaroons[ts]["authn"]) // We should authenticate as bob (because macaroons are presented ordered // by "cookie"name) authInfo, err = client3.do(testContext, ts, identchecker.LoginOp) c.Assert(err, qt.IsNil) c.Assert(authInfo.Identity, qt.Equals, identchecker.SimpleIdentity("bob")) c.Assert(authInfo.Used, qt.DeepEquals, []bool{true, false}) // Try them the other way around and we should authenticate as alice. client3 = newClient(locator) client3.addMacaroon(ts, "1.alice", client2.macaroons[ts]["authn"]) client3.addMacaroon(ts, "2.bob", client1.macaroons[ts]["authn"]) authInfo, err = client3.do(testContext, ts, identchecker.LoginOp) c.Assert(err, qt.IsNil) c.Assert(authInfo.Identity, qt.Equals, identchecker.SimpleIdentity("alice")) c.Assert(authInfo.Used, qt.DeepEquals, []bool{true, false}) } func TestMacaroonOpsFatalError(t *testing.T) { c := qt.New(t) // When we get a non-VerificationError error from the // oven, we don't do any more verification. checker := identchecker.NewChecker(identchecker.CheckerParams{ MacaroonVerifier: macaroonVerifierWithError{errgo.New("an error")}, }) m, err := macaroon.New(nil, nil, "", macaroon.V2) c.Assert(err, qt.IsNil) _, err = checker.Auth(macaroon.Slice{m}).Allow(testContext, identchecker.LoginOp) c.Assert(err, qt.ErrorMatches, `cannot retrieve macaroon: an error`) } func macaroonConditions(caveats []macaroon.Caveat, allowThird bool) []string { conds := make([]string, len(caveats)) for i, cav := range caveats { if cav.Location != "" { if !allowThird { panic("found unexpected third party caveat") } continue } conds[i] = string(cav.Id) } return conds } func readOp(entity string) bakery.Op { return bakery.Op{ Entity: entity, Action: "read", } } func writeOp(entity string) bakery.Op { return bakery.Op{ Entity: entity, Action: "write", } } // opACL implements identchecker.Authorizer by looking the operation // up in the given map. If the username is in the associated slice // or the slice contains "everyone", authorization is granted. type opACL map[bakery.Op][]string func (auth opACL) Authorize(ctx context.Context, id identchecker.Identity, ops []bakery.Op) (allowed []bool, caveats []checkers.Caveat, err error) { return identchecker.ACLAuthorizer{ GetACL: func(ctx context.Context, op bakery.Op) ([]string, bool, error) { return auth[op], true, nil }, }.Authorize(ctx, id, ops) } type idService struct { location string *discharger discharges []dischargeRecord } func newIdService(location string, locator dischargerLocator) *idService { ids := &idService{ location: location, } key := mustGenerateKey() ids.discharger = &discharger{ key: key, checker: ids, locator: locator, } locator[location] = ids.discharger return ids } func (ids *idService) CheckThirdPartyCaveat(ctx context.Context, info *bakery.ThirdPartyCaveatInfo) ([]checkers.Caveat, error) { if string(info.Condition) != "is-authenticated-user" { return nil, errgo.Newf("third party condition not recognized") } username := dischargeUserFromContext(ctx) if username == "" { return nil, errgo.Newf("no current user") } ids.discharges = append(ids.discharges, dischargeRecord{ Location: ids.location, User: username, }) return []checkers.Caveat{ checkers.DeclaredCaveat("username", username), }, nil } func (ids *idService) IdentityFromContext(ctx context.Context) (identchecker.Identity, []checkers.Caveat, error) { return nil, []checkers.Caveat{{ Location: ids.location, Condition: "is-authenticated-user", }}, nil } func (ids *idService) DeclaredIdentity(ctx context.Context, declared map[string]string) (identchecker.Identity, error) { user, ok := declared["username"] if !ok { return nil, errgo.Newf("no username declared") } return identchecker.SimpleIdentity(user), nil } type dischargeUserKey struct{} func contextWithDischargeUser(ctx context.Context, username string) context.Context { return context.WithValue(ctx, dischargeUserKey{}, username) } func dischargeUserFromContext(ctx context.Context) string { username, _ := ctx.Value(dischargeUserKey{}).(string) return username } type basicAuthIdService struct{} func (basicAuthIdService) IdentityFromContext(ctx context.Context) (identchecker.Identity, []checkers.Caveat, error) { user, pass := basicAuthFromContext(ctx) if user != "sherlock" || pass != "holmes" { return nil, nil, nil } return identchecker.SimpleIdentity(user), nil, nil } func (basicAuthIdService) DeclaredIdentity(ctx context.Context, declared map[string]string) (identchecker.Identity, error) { return nil, errgo.Newf("no identity declarations in basic auth id service") } // service represents a service that requires authorization. // Clients can make requests to the service to perform operations // and may receive a macaroon to discharge if the authorization // process requires it. type service struct { checker *identchecker.Checker oven *bakery.Oven } func newService(auth identchecker.Authorizer, idm identchecker.IdentityClient, locator bakery.ThirdPartyLocator) *service { oven := newMacaroonStore(mustGenerateKey(), locator) return &service{ checker: identchecker.NewChecker(identchecker.CheckerParams{ Checker: testChecker, Authorizer: auth, IdentityClient: idm, MacaroonVerifier: oven, }), oven: oven, } } // do makes a request to the service to perform the given operations // using the given macaroons for authorization. // It may return a dischargeRequiredError containing a macaroon // that needs to be discharged. func (svc *service) do(ctx context.Context, ms []macaroon.Slice, ops ...bakery.Op) (*identchecker.AuthInfo, error) { authInfo, err := svc.checker.Auth(ms...).Allow(ctx, ops...) return authInfo, svc.maybeDischargeRequiredError(err) } func (svc *service) capability(ctx context.Context, ms []macaroon.Slice, ops ...bakery.Op) (*bakery.Macaroon, error) { ai, err := svc.checker.Auth(ms...).Allow(ctx, ops...) if err != nil { return nil, svc.maybeDischargeRequiredError(err) } m, err := svc.oven.NewMacaroon(ctx, bakery.LatestVersion, nil, ops...) if err != nil { return nil, errgo.Mask(err) } for _, cond := range ai.Conditions() { if strings.HasPrefix(cond, "declared ") { // TODO check namespace too. continue } if err := m.M().AddFirstPartyCaveat([]byte(cond)); err != nil { return nil, errgo.Mask(err) } } return m, nil } func (svc *service) maybeDischargeRequiredError(err error) error { derr, ok := errgo.Cause(err).(*bakery.DischargeRequiredError) if !ok { return errgo.Mask(err) } // If we're making a login macaroon, it should have ForAuthentication // set in derr. for _, op := range derr.Ops { if op == identchecker.LoginOp && !derr.ForAuthentication { panic(errgo.Newf("found discharge-required error for LoginOp without ForAuthentication")) } } m, err := svc.oven.NewMacaroon(testContext, bakery.LatestVersion, derr.Caveats, derr.Ops...) if err != nil { return errgo.Mask(err) } name := "authz" if len(derr.Ops) == 1 && derr.Ops[0] == identchecker.LoginOp { name = "authn" } return &dischargeRequiredError{ name: name, m: m, } } type discharger struct { key *bakery.KeyPair locator bakery.ThirdPartyLocator checker bakery.ThirdPartyCaveatChecker } type dischargeRequiredError struct { name string m *bakery.Macaroon } func (*dischargeRequiredError) Error() string { return "discharge required" } func (d *discharger) discharge(ctx context.Context, cav macaroon.Caveat, payload []byte) (*bakery.Macaroon, error) { m, err := bakery.Discharge(ctx, bakery.DischargeParams{ Id: cav.Id, Caveat: payload, Key: d.key, Checker: d.checker, Locator: d.locator, }) if err != nil { return nil, errgo.Mask(err) } return m, nil } type dischargerLocator map[string]*discharger // ThirdPartyInfo implements the bakery.ThirdPartyLocator interface. func (l dischargerLocator) ThirdPartyInfo(ctx context.Context, loc string) (bakery.ThirdPartyInfo, error) { d, ok := l[loc] if !ok { return bakery.ThirdPartyInfo{}, bakery.ErrNotFound } return bakery.ThirdPartyInfo{ PublicKey: d.key.Public, Version: bakery.LatestVersion, }, nil } type hierarchicalOpsAuthorizer struct{} func (a hierarchicalOpsAuthorizer) AuthorizeOps(ctx context.Context, authorizedOp bakery.Op, queryOps []bakery.Op) ([]bool, []checkers.Caveat, error) { ok := make([]bool, len(queryOps)) for i, op := range queryOps { if isParentPathEntity(authorizedOp.Entity, op.Entity) && (authorizedOp.Action == op.Action || authorizedOp.Action == "*") { ok[i] = true } } return ok, nil, nil } // isParentPathEntity reports whether both entity1 and entity2 // represent paths and entity1 is a parent of entity2. func isParentPathEntity(entity1, entity2 string) bool { path1, path2 := strings.TrimPrefix(entity1, "path-"), strings.TrimPrefix(entity2, "path-") if len(path1) == len(entity1) || len(path2) == len(entity2) { return false } if !strings.HasPrefix(path2, path1) { return false } if len(path1) == len(path2) { return true } return path2[len(path1)] == '/' } type client struct { key *bakery.KeyPair macaroons map[*service]map[string]macaroon.Slice dischargers dischargerLocator } func newClient(dischargers dischargerLocator) *client { return &client{ key: mustGenerateKey(), dischargers: dischargers, // macaroons holds the macaroons applicable to each service. // This is the test equivalent of the cookie jar used by httpbakery. macaroons: make(map[*service]map[string]macaroon.Slice), } } const maxRetries = 3 // do performs a set of operations on the given service. // It includes all the macaroons in c.macaroons[svc] as authorization // information on the request. func (c *client) do(ctx context.Context, svc *service, ops ...bakery.Op) (*identchecker.AuthInfo, error) { var authInfo *identchecker.AuthInfo err := c.doFunc(ctx, svc, func(ms []macaroon.Slice) (err error) { authInfo, err = svc.do(ctx, ms, ops...) return }) return authInfo, err } // capability returns a capability macaroon for the given operations. func (c *client) capability(ctx context.Context, svc *service, ops ...bakery.Op) (*bakery.Macaroon, error) { var m *bakery.Macaroon err := c.doFunc(ctx, svc, func(ms []macaroon.Slice) (err error) { m, err = svc.capability(ctx, ms, ops...) return }) return m, err } func (c *client) dischargedCapability(ctx context.Context, svc *service, ops ...bakery.Op) (macaroon.Slice, error) { m, err := c.capability(ctx, svc, ops...) if err != nil { return nil, errgo.Mask(err) } return c.dischargeAll(ctx, m) } func (c *client) doFunc(ctx context.Context, svc *service, f func(ms []macaroon.Slice) error) error { for i := 0; i < maxRetries; i++ { err := f(c.requestMacaroons(svc)) derr, ok := errgo.Cause(err).(*dischargeRequiredError) if !ok { return err } ms, err := c.dischargeAll(ctx, derr.m) if err != nil { return errgo.Mask(err) } c.addMacaroon(svc, derr.name, ms) } return errgo.New("discharge failed too many times") } func (c *client) clearMacaroons(svc *service) { if svc == nil { c.macaroons = make(map[*service]map[string]macaroon.Slice) return } delete(c.macaroons, svc) } func (c *client) addMacaroon(svc *service, name string, m macaroon.Slice) { if c.macaroons[svc] == nil { c.macaroons[svc] = make(map[string]macaroon.Slice) } c.macaroons[svc][name] = m } func (c *client) requestMacaroons(svc *service) []macaroon.Slice { mmap := c.macaroons[svc] // Put all the macaroons in the slice ordered by key // so that we have deterministic behaviour in the tests. names := make([]string, 0, len(mmap)) for name := range mmap { names = append(names, name) } sort.Strings(names) ms := make([]macaroon.Slice, len(names)) for i, name := range names { ms[i] = mmap[name] } return ms } func (c *client) dischargeAll(ctx context.Context, m *bakery.Macaroon) (macaroon.Slice, error) { return bakery.DischargeAll(ctx, m, func(ctx context.Context, cav macaroon.Caveat, payload []byte) (*bakery.Macaroon, error) { d := c.dischargers[cav.Location] if d == nil { return nil, errgo.Newf("third party discharger %q not found", cav.Location) } return d.discharge(ctx, cav, payload) }) } func newMacaroonStore(key *bakery.KeyPair, locator bakery.ThirdPartyLocator) *bakery.Oven { return bakery.NewOven(bakery.OvenParams{ Key: key, Locator: locator, }) } // macaroonVerifierWithError is an implementation of MacaroonVerifier that // returns the given error on all store operations. type macaroonVerifierWithError struct { err error } func (s macaroonVerifierWithError) VerifyMacaroon(ctx context.Context, ms macaroon.Slice) (ops []bakery.Op, conditions []string, err error) { return nil, nil, errgo.Mask(s.err, errgo.Any) } macaroon-bakery-3.0.2/bakery/identchecker/common_test.go000066400000000000000000000027641464427415500233760ustar00rootroot00000000000000package identchecker_test import ( "context" "encoding/json" "time" "gopkg.in/macaroon.v2" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/checkers" ) // testContext holds the testing background context - its associated time when checking // time-before caveats will always be the value of epoch. var testContext = checkers.ContextWithClock(context.Background(), stoppedClock{epoch}) var ( epoch = time.Date(1900, 11, 17, 19, 00, 13, 0, time.UTC) ) var testChecker = func() *checkers.Checker { c := checkers.New(nil) c.Namespace().Register("testns", "") c.Register("true", "testns", trueCheck) return c }() // trueCheck always succeeds. func trueCheck(ctx context.Context, cond, args string) error { return nil } func macStr(m *macaroon.Macaroon) string { data, err := json.MarshalIndent(m, "\t", "\t") if err != nil { panic(err) } return string(data) } type stoppedClock struct { t time.Time } func (t stoppedClock) Now() time.Time { return t.t } type basicAuthKey struct{} type basicAuth struct { user, password string } func contextWithBasicAuth(ctx context.Context, user, password string) context.Context { return context.WithValue(ctx, basicAuthKey{}, basicAuth{user, password}) } func basicAuthFromContext(ctx context.Context) (user, password string) { auth, _ := ctx.Value(basicAuthKey{}).(basicAuth) return auth.user, auth.password } func mustGenerateKey() *bakery.KeyPair { return bakery.MustGenerateKey() } macaroon-bakery-3.0.2/bakery/identchecker/identity.go000066400000000000000000000060441464427415500226730ustar00rootroot00000000000000package identchecker import ( "context" "gopkg.in/errgo.v1" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/checkers" ) // IdentityClient represents an abstract identity manager. User // identities can be based on local informaton (for example // HTTP basic auth) or by reference to an external trusted // third party (an identity manager). type IdentityClient interface { // IdentityFromContext returns the identity based on information in the context. // If it cannot determine the identity based on the context, then it // should return a set of caveats containing a third party caveat that, // when discharged, can be used to obtain the identity with DeclaredIdentity. // // It should only return an error if it cannot check the identity // (for example because of a database access error) - it's // OK to return all zero values when there's // no identity found and no third party to address caveats to. IdentityFromContext(ctx context.Context) (Identity, []checkers.Caveat, error) // DeclaredIdentity parses the identity declaration from the given // declared attributes. // TODO take the set of first party caveat conditions instead? DeclaredIdentity(ctx context.Context, declared map[string]string) (Identity, error) } // Identity holds identity information declared in a first party caveat // added when discharging a third party caveat. type Identity interface { // Id returns the id of the user, which may be an // opaque blob with no human meaning. // An id is only considered to be unique // with a given domain. Id() string // Domain holds the domain of the user. This // will be empty if the user was authenticated // directly with the identity provider. Domain() string } // noIdentities defines the null identity provider - it never returns any identities. type noIdentities struct{} // IdentityFromContext implements IdentityClient.IdentityFromContext by // never returning a declared identity or any caveats. func (noIdentities) IdentityFromContext(ctx context.Context) (Identity, []checkers.Caveat, error) { return nil, nil, nil } // DeclaredIdentity implements IdentityClient.DeclaredIdentity by // always returning an error. func (noIdentities) DeclaredIdentity(ctx context.Context, declared map[string]string) (Identity, error) { return nil, errgo.Newf("no identity declared or possible") } var _ ACLIdentity = SimpleIdentity("") // SimpleIdentity implements a simple form of identity where // the user is represented by a string. type SimpleIdentity string // Domain implements Identity.Domain by always // returning the empty domain. func (SimpleIdentity) Domain() string { return "" } // Id returns id as a string. func (id SimpleIdentity) Id() string { return string(id) } // Allow implements ACLIdentity by allowing the identity access to // ACL members that are equal to id. That is, some user u is considered // a member of group u and no other. func (id SimpleIdentity) Allow(ctx context.Context, acl []string) (bool, error) { for _, g := range acl { if string(id) == g { return true, nil } } return false, nil } macaroon-bakery-3.0.2/bakery/keys.go000066400000000000000000000130421464427415500173610ustar00rootroot00000000000000package bakery import ( "context" "crypto/rand" "encoding/base64" "encoding/json" "strings" "sync" "golang.org/x/crypto/curve25519" "golang.org/x/crypto/nacl/box" "gopkg.in/errgo.v1" "gopkg.in/macaroon.v2" ) // KeyLen is the byte length of the Ed25519 public and private keys used for // caveat id encryption. const KeyLen = 32 // NonceLen is the byte length of the nonce values used for caveat id // encryption. const NonceLen = 24 // PublicKey is a 256-bit Ed25519 public key. type PublicKey struct { Key } // PrivateKey is a 256-bit Ed25519 private key. type PrivateKey struct { Key } // Public derives the public key from a private key. func (k PrivateKey) Public() PublicKey { var pub PublicKey curve25519.ScalarBaseMult((*[32]byte)(&pub.Key), (*[32]byte)(&k.Key)) return pub } // Key is a 256-bit Ed25519 key. type Key [KeyLen]byte // String returns the base64 representation of the key. func (k Key) String() string { return base64.StdEncoding.EncodeToString(k[:]) } // MarshalBinary implements encoding.BinaryMarshaler.MarshalBinary. func (k Key) MarshalBinary() ([]byte, error) { return k[:], nil } // isZero reports whether the key consists entirely of zeros. func (k Key) isZero() bool { return k == Key{} } // UnmarshalBinary implements encoding.BinaryUnmarshaler.UnmarshalBinary. func (k *Key) UnmarshalBinary(data []byte) error { if len(data) != len(k) { return errgo.Newf("wrong length for key, got %d want %d", len(data), len(k)) } copy(k[:], data) return nil } // MarshalText implements encoding.TextMarshaler.MarshalText. func (k Key) MarshalText() ([]byte, error) { data := make([]byte, base64.StdEncoding.EncodedLen(len(k))) base64.StdEncoding.Encode(data, k[:]) return data, nil } // boxKey returns the box package's type for a key. func (k Key) boxKey() *[KeyLen]byte { return (*[KeyLen]byte)(&k) } // UnmarshalText implements encoding.TextUnmarshaler.UnmarshalText. func (k *Key) UnmarshalText(text []byte) error { data, err := macaroon.Base64Decode(text) if err != nil { return errgo.Notef(err, "cannot decode base64 key") } if len(data) != len(k) { return errgo.Newf("wrong length for key, got %d want %d", len(data), len(k)) } copy(k[:], data) return nil } // ThirdPartyInfo holds information on a given third party // discharge service. type ThirdPartyInfo struct { // PublicKey holds the public key of the third party. PublicKey PublicKey // Version holds latest the bakery protocol version supported // by the discharger. Version Version } // ThirdPartyLocator is used to find information on third // party discharge services. type ThirdPartyLocator interface { // ThirdPartyInfo returns information on the third // party at the given location. It returns ErrNotFound if no match is found. // This method must be safe to call concurrently. ThirdPartyInfo(ctx context.Context, loc string) (ThirdPartyInfo, error) } // ThirdPartyStore implements a simple ThirdPartyLocator. // A trailing slash on locations is ignored. type ThirdPartyStore struct { mu sync.RWMutex m map[string]ThirdPartyInfo } // NewThirdPartyStore returns a new instance of ThirdPartyStore // that stores locations in memory. func NewThirdPartyStore() *ThirdPartyStore { return &ThirdPartyStore{ m: make(map[string]ThirdPartyInfo), } } // AddInfo associates the given information with the // given location, ignoring any trailing slash. // This method is OK to call concurrently with sThirdPartyInfo. func (s *ThirdPartyStore) AddInfo(loc string, info ThirdPartyInfo) { s.mu.Lock() defer s.mu.Unlock() s.m[canonicalLocation(loc)] = info } func canonicalLocation(loc string) string { return strings.TrimSuffix(loc, "/") } // ThirdPartyInfo implements the ThirdPartyLocator interface. func (s *ThirdPartyStore) ThirdPartyInfo(ctx context.Context, loc string) (ThirdPartyInfo, error) { s.mu.RLock() defer s.mu.RUnlock() if info, ok := s.m[canonicalLocation(loc)]; ok { return info, nil } return ThirdPartyInfo{}, ErrNotFound } // KeyPair holds a public/private pair of keys. type KeyPair struct { Public PublicKey `json:"public"` Private PrivateKey `json:"private"` } // UnmarshalJSON implements json.Unmarshaler. func (k *KeyPair) UnmarshalJSON(data []byte) error { type keyPair KeyPair if err := json.Unmarshal(data, (*keyPair)(k)); err != nil { return err } return k.validate() } // UnmarshalYAML implements yaml.Unmarshaler. func (k *KeyPair) UnmarshalYAML(unmarshal func(interface{}) error) error { type keyPair KeyPair if err := unmarshal((*keyPair)(k)); err != nil { return err } return k.validate() } func (k *KeyPair) validate() error { if k.Public.isZero() { return errgo.Newf("missing public key") } if k.Private.isZero() { return errgo.Newf("missing private key") } return nil } // GenerateKey generates a new key pair. func GenerateKey() (*KeyPair, error) { var key KeyPair pub, priv, err := box.GenerateKey(rand.Reader) if err != nil { return nil, err } key.Public = PublicKey{*pub} key.Private = PrivateKey{*priv} return &key, nil } // MustGenerateKey is like GenerateKey but panics if GenerateKey returns // an error - useful in tests. func MustGenerateKey() *KeyPair { key, err := GenerateKey() if err != nil { panic(errgo.Notef(err, "cannot generate key")) } return key } // String implements the fmt.Stringer interface // by returning the base64 representation of the // public key part of key. func (key *KeyPair) String() string { return key.Public.String() } type emptyLocator struct{} func (emptyLocator) ThirdPartyInfo(context.Context, string) (ThirdPartyInfo, error) { return ThirdPartyInfo{}, ErrNotFound } macaroon-bakery-3.0.2/bakery/keys_test.go000066400000000000000000000077631464427415500204350ustar00rootroot00000000000000package bakery_test import ( "encoding/base64" "encoding/json" "testing" qt "github.com/frankban/quicktest" "gopkg.in/yaml.v2" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" ) var testKey = newTestKey(0) func TestMarshalBinary(t *testing.T) { c := qt.New(t) data, err := testKey.MarshalBinary() c.Assert(err, qt.IsNil) c.Assert(data, qt.DeepEquals, []byte(testKey[:])) var key1 bakery.Key err = key1.UnmarshalBinary(data) c.Assert(err, qt.IsNil) c.Assert(key1, qt.DeepEquals, testKey) } func TestMarshalText(t *testing.T) { c := qt.New(t) data, err := testKey.MarshalText() c.Assert(err, qt.IsNil) c.Assert(string(data), qt.Equals, base64.StdEncoding.EncodeToString([]byte(testKey[:]))) var key1 bakery.Key err = key1.UnmarshalText(data) c.Assert(err, qt.IsNil) c.Assert(key1, qt.Equals, testKey) } func TestUnmarshalTextWrongKeyLength(t *testing.T) { c := qt.New(t) var key bakery.Key err := key.UnmarshalText([]byte("aGVsbG8K")) c.Assert(err, qt.ErrorMatches, `wrong length for key, got 6 want 32`) } func TestKeyPairMarshalJSON(t *testing.T) { c := qt.New(t) kp := bakery.KeyPair{ Public: bakery.PublicKey{testKey}, Private: bakery.PrivateKey{testKey}, } kp.Private.Key[0] = 99 data, err := json.Marshal(kp) c.Assert(err, qt.IsNil) var x map[string]interface{} err = json.Unmarshal(data, &x) c.Assert(err, qt.IsNil) // Check that the fields have marshaled as strings. _, ok := x["private"].(string) c.Assert(ok, qt.Equals, true) _, ok = x["public"].(string) c.Assert(ok, qt.Equals, true) var kp1 bakery.KeyPair err = json.Unmarshal(data, &kp1) c.Assert(err, qt.IsNil) c.Assert(kp1, qt.DeepEquals, kp) } func TestKeyPairMarshalYAML(t *testing.T) { c := qt.New(t) kp := bakery.KeyPair{ Public: bakery.PublicKey{testKey}, Private: bakery.PrivateKey{testKey}, } kp.Private.Key[0] = 99 data, err := yaml.Marshal(kp) c.Assert(err, qt.IsNil) var x map[string]interface{} err = yaml.Unmarshal(data, &x) c.Assert(err, qt.IsNil) // Check that the fields have marshaled as strings. _, ok := x["private"].(string) c.Assert(ok, qt.Equals, true) _, ok = x["public"].(string) c.Assert(ok, qt.Equals, true) var kp1 bakery.KeyPair err = yaml.Unmarshal(data, &kp1) c.Assert(err, qt.IsNil) c.Assert(kp1, qt.DeepEquals, kp) } func TestKeyPairUnmarshalJSONMissingPublicKey(t *testing.T) { c := qt.New(t) data := `{"private": "7ZcOvDAW9opAIPzJ7FdSbz2i2qL8bFZapDlmNLpMzpU="}` var k bakery.KeyPair err := json.Unmarshal([]byte(data), &k) c.Assert(err, qt.ErrorMatches, `missing public key`) } func TestKeyPairUnmarshalJSONMissingPrivateKey(t *testing.T) { c := qt.New(t) data := `{"public": "7ZcOvDAW9opAIPzJ7FdSbz2i2qL8bFZapDlmNLpMzpU="}` var k bakery.KeyPair err := json.Unmarshal([]byte(data), &k) c.Assert(err, qt.ErrorMatches, `missing private key`) } func TestKeyPairUnmarshalJSONEmptyKeys(t *testing.T) { c := qt.New(t) data := `{"private": "", "public": ""}` var k bakery.KeyPair err := json.Unmarshal([]byte(data), &k) c.Assert(err, qt.ErrorMatches, `wrong length for key, got 0 want 32`) } func TestKeyPairUnmarshalJSONNoKeys(t *testing.T) { c := qt.New(t) data := `{}` var k bakery.KeyPair err := json.Unmarshal([]byte(data), &k) c.Assert(err, qt.ErrorMatches, `missing public key`) } func TestKeyPairUnmarshalYAMLMissingPublicKey(t *testing.T) { c := qt.New(t) data := ` private: 7ZcOvDAW9opAIPzJ7FdSbz2i2qL8bFZapDlmNLpMzpU= ` var k bakery.KeyPair err := yaml.Unmarshal([]byte(data), &k) c.Assert(err, qt.ErrorMatches, `missing public key`) } func TestKeyPairUnmarshalYAMLMissingPrivateKey(t *testing.T) { c := qt.New(t) data := ` public: 7ZcOvDAW9opAIPzJ7FdSbz2i2qL8bFZapDlmNLpMzpU= ` var k bakery.KeyPair err := yaml.Unmarshal([]byte(data), &k) c.Assert(err, qt.ErrorMatches, `missing private key`) } func TestDerivePublicFromPrivate(t *testing.T) { c := qt.New(t) k := mustGenerateKey() c.Assert(k.Private.Public(), qt.Equals, k.Public) } func newTestKey(n byte) bakery.Key { var k bakery.Key for i := range k { k[i] = n + byte(i) } return k } macaroon-bakery-3.0.2/bakery/logger.go000066400000000000000000000013701464427415500176660ustar00rootroot00000000000000package bakery import ( "context" ) // Logger is used by the bakery to log informational messages // about bakery operations. type Logger interface { Infof(ctx context.Context, f string, args ...interface{}) Debugf(ctx context.Context, f string, args ...interface{}) } // DefaultLogger returns a Logger instance that does nothing. // // Deprecated: DefaultLogger exists for historical compatibility // only. Previously it logged using github.com/juju/loggo. func DefaultLogger(name string) Logger { return nopLogger{} } type nopLogger struct{} // Debugf implements Logger.Debugf. func (nopLogger) Debugf(context.Context, string, ...interface{}) {} // Debugf implements Logger.Infof. func (nopLogger) Infof(context.Context, string, ...interface{}) {} macaroon-bakery-3.0.2/bakery/macaroon.go000066400000000000000000000271121464427415500202100ustar00rootroot00000000000000package bakery import ( "bytes" "context" "encoding/base64" "encoding/binary" "encoding/json" "gopkg.in/errgo.v1" "gopkg.in/macaroon.v2" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/checkers" ) // legacyNamespace holds the standard namespace as used by // pre-version3 macaroons. func legacyNamespace() *checkers.Namespace { ns := checkers.NewNamespace(nil) ns.Register(checkers.StdNamespace, "") return ns } // Macaroon represents an undischarged macaroon along with its first // party caveat namespace and associated third party caveat information // which should be passed to the third party when discharging a caveat. type Macaroon struct { // m holds the underlying macaroon. m *macaroon.Macaroon // version holds the version of the macaroon. version Version // caveatData maps from a third party caveat id to its // associated information, usually public-key encrypted with the // third party's public key. // // If version is less than Version3, this will always be nil, // because clients prior to that version do not support // macaroon-external caveat ids. caveatData map[string][]byte // namespace holds the first-party caveat namespace of the macaroon. namespace *checkers.Namespace // caveatIdPrefix holds the prefix to use for the ids of any third // party caveats created. This can be set when Discharge creates a // discharge macaroon. caveatIdPrefix []byte } // NewLegacyMacaroon returns a new macaroon holding m. // This should only be used when there's no alternative // (for example when m has been unmarshaled // from some alternative format). func NewLegacyMacaroon(m *macaroon.Macaroon) (*Macaroon, error) { v, err := bakeryVersion(m.Version()) if err != nil { return nil, errgo.Mask(err) } return &Macaroon{ m: m, version: v, namespace: legacyNamespace(), }, nil } type macaroonJSON struct { Macaroon *macaroon.Macaroon `json:"m"` Version Version `json:"v"` // Note: CaveatData is encoded using URL-base64-encoded keys // because JSON cannot deal with arbitrary byte sequences // in its strings, and URL-base64 values to match the // standard macaroon encoding. CaveatData map[string]string `json:"cdata,omitempty"` Namespace *checkers.Namespace `json:"ns"` } // Clone returns a copy of the macaroon. Note that the the new // macaroon's namespace still points to the same underlying Namespace - // copying the macaroon does not make a copy of the namespace. func (m *Macaroon) Clone() *Macaroon { m1 := *m m1.m = m1.m.Clone() m1.caveatData = make(map[string][]byte) for id, data := range m.caveatData { m1.caveatData[id] = data } return &m1 } // MarshalJSON implements json.Marshaler by marshaling // the macaroon into the original macaroon format if the // version is earlier than Version3. func (m *Macaroon) MarshalJSON() ([]byte, error) { if m.version < Version3 { if len(m.caveatData) > 0 { return nil, errgo.Newf("cannot marshal pre-version3 macaroon with external caveat data") } return m.m.MarshalJSON() } caveatData := make(map[string]string) for id, data := range m.caveatData { caveatData[base64.RawURLEncoding.EncodeToString([]byte(id))] = base64.RawURLEncoding.EncodeToString(data) } return json.Marshal(macaroonJSON{ Macaroon: m.m, Version: m.version, CaveatData: caveatData, Namespace: m.namespace, }) } // UnmarshalJSON implements json.Unmarshaler by unmarshaling in a // backwardly compatible way - if provided with a previous macaroon // version, it will unmarshal that too. func (m *Macaroon) UnmarshalJSON(data []byte) error { // First try with new data format. var m1 macaroonJSON if err := json.Unmarshal(data, &m1); err != nil { // If we get an unmarshal error, we won't be able // to unmarshal into the old format either, as extra fields // are ignored. return errgo.Mask(err) } if m1.Macaroon == nil { return m.unmarshalJSONOldFormat(data) } // We've got macaroon field - it's the new format. if m1.Version < Version3 || m1.Version > LatestVersion { return errgo.Newf("unexpected bakery macaroon version; got %d want %d", m1.Version, Version3) } if got, want := m1.Macaroon.Version(), MacaroonVersion(m1.Version); got != want { return errgo.Newf("underlying macaroon has inconsistent version; got %d want %d", got, want) } caveatData := make(map[string][]byte) for id64, data64 := range m1.CaveatData { id, err := macaroon.Base64Decode([]byte(id64)) if err != nil { return errgo.Notef(err, "cannot decode caveat id") } data, err := macaroon.Base64Decode([]byte(data64)) if err != nil { return errgo.Notef(err, "cannot decode caveat") } caveatData[string(id)] = data } m.caveatData = caveatData m.m = m1.Macaroon m.namespace = m1.Namespace // TODO should we allow version > LatestVersion here? m.version = m1.Version return nil } // unmarshalJSONOldFormat unmarshals the data from an old format // macaroon (without any external caveats or namespace). func (m *Macaroon) unmarshalJSONOldFormat(data []byte) error { // Try to unmarshal from the original format. var m1 *macaroon.Macaroon if err := json.Unmarshal(data, &m1); err != nil { return errgo.Mask(err) } m2, err := NewLegacyMacaroon(m1) if err != nil { return errgo.Mask(err) } *m = *m2 return nil } // bakeryVersion returns a bakery version that corresponds to // the macaroon version v. It is necessarily approximate because // several bakery versions can correspond to a single macaroon // version, so it's only of use when decoding legacy formats // (in Macaroon.UnmarshalJSON). // // It will return an error if it doesn't recognize the version. func bakeryVersion(v macaroon.Version) (Version, error) { switch v { case macaroon.V1: // Use version 1 because we don't know of any existing // version 0 clients. return Version1, nil case macaroon.V2: // Note that this could also correspond to Version3, but // this logic is explicitly for legacy versions. return Version2, nil default: return 0, errgo.Newf("unknown macaroon version when legacy-unmarshaling bakery macaroon; got %d", v) } } // NewMacaroon creates and returns a new macaroon with the given root // key, id and location. If the version is more than the latest known // version, the latest known version will be used. The namespace is that // of the service creating it. func NewMacaroon(rootKey, id []byte, location string, version Version, ns *checkers.Namespace) (*Macaroon, error) { if version > LatestVersion { version = LatestVersion } m, err := macaroon.New(rootKey, id, location, MacaroonVersion(version)) if err != nil { return nil, errgo.Notef(err, "cannot create macaroon") } return &Macaroon{ m: m, version: version, namespace: ns, }, nil } // M returns the underlying macaroon held within m. func (m *Macaroon) M() *macaroon.Macaroon { return m.m } // Version returns the bakery version of the first party // that created the macaroon. func (m *Macaroon) Version() Version { return m.version } // Namespace returns the first party caveat namespace of the macaroon. func (m *Macaroon) Namespace() *checkers.Namespace { return m.namespace } // AddCaveats is a convenienced method that calls m.AddCaveat for each // caveat in cavs. func (m *Macaroon) AddCaveats(ctx context.Context, cavs []checkers.Caveat, key *KeyPair, loc ThirdPartyLocator) error { for _, cav := range cavs { if err := m.AddCaveat(ctx, cav, key, loc); err != nil { return errgo.Notef(err, "cannot add caveat %#v", cav) } } return nil } // AddCaveat adds a caveat to the given macaroon. // // If it's a third-party caveat, it encrypts it using the given key pair // and by looking up the location using the given locator. If it's a // first party cavat, key and loc are unused. // // As a special case, if the caveat's Location field has the prefix // "local " the caveat is added as a client self-discharge caveat using // the public key base64-encoded in the rest of the location. In this // case, the Condition field must be empty. The resulting third-party // caveat will encode the condition "true" encrypted with that public // key. See LocalThirdPartyCaveat for a way of creating such caveats. func (m *Macaroon) AddCaveat(ctx context.Context, cav checkers.Caveat, key *KeyPair, loc ThirdPartyLocator) error { if cav.Location == "" { if err := m.m.AddFirstPartyCaveat([]byte(m.namespace.ResolveCaveat(cav).Condition)); err != nil { return errgo.Mask(err) } return nil } if key == nil { return errgo.Newf("no private key to encrypt third party caveat") } var info ThirdPartyInfo if localInfo, ok := parseLocalLocation(cav.Location); ok { info = localInfo cav.Location = "local" if cav.Condition != "" { return errgo.New("cannot specify caveat condition in local third-party caveat") } cav.Condition = "true" } else { if loc == nil { return errgo.Newf("no locator when adding third party caveat") } var err error info, err = loc.ThirdPartyInfo(ctx, cav.Location) if err != nil { return errgo.Notef(err, "cannot find public key for location %q", cav.Location) } } rootKey, err := randomBytes(24) if err != nil { return errgo.Notef(err, "cannot generate third party secret") } // Use the least supported version to encode the caveat. if m.version < info.Version { info.Version = m.version } caveatInfo, err := encodeCaveat(cav.Condition, rootKey, info, key, m.namespace) if err != nil { return errgo.Notef(err, "cannot create third party caveat at %q", cav.Location) } var id []byte if info.Version < Version3 { // We're encoding for an earlier client or third party which does // not understand bundled caveat info, so use the encoded // caveat information as the caveat id. id = caveatInfo } else { id = m.newCaveatId(m.caveatIdPrefix) if m.caveatData == nil { m.caveatData = make(map[string][]byte) } m.caveatData[string(id)] = caveatInfo } if err := m.m.AddThirdPartyCaveat(rootKey, id, cav.Location); err != nil { return errgo.Notef(err, "cannot add third party caveat") } return nil } // newCaveatId returns a third party caveat id that // does not duplicate any third party caveat ids already inside m. // // If base is non-empty, it is used as the id prefix. func (m *Macaroon) newCaveatId(base []byte) []byte { var id []byte if len(base) > 0 { id = make([]byte, len(base), len(base)+binary.MaxVarintLen64) copy(id, base) } else { id = make([]byte, 0, 1+binary.MaxVarintLen32) // Add a version byte to the caveat id. Technically // this is unnecessary as the caveat-decoding logic // that looks at versions should never see this id, // but if the caveat payload isn't provided with the // payload, having this version gives a strong indication // that the payload has been omitted so we can produce // a better error for the user. id = append(id, byte(Version3)) } // Iterate through integers looking for one that isn't already used, // starting from n so that if everyone is using this same algorithm, // we'll only perform one iteration. // // Note that although this looks like an infinite loop, // there's no way that it can run for more iterations // than the total number of existing third party caveats, // whatever their ids. caveats := m.m.Caveats() again: for i := len(m.caveatData); ; i++ { // We append a varint to the end of the id and assume that // any client that's created the id that we're using as a base // is using similar conventions - in the worst case they might // end up with a duplicate third party caveat id and thus create // a macaroon that cannot be discharged. id1 := appendUvarint(id, uint64(i)) for _, cav := range caveats { if cav.VerificationId != nil && bytes.Equal(cav.Id, id1) { continue again } } return id1 } } macaroon-bakery-3.0.2/bakery/macaroon_test.go000066400000000000000000000167351464427415500212600ustar00rootroot00000000000000package bakery_test import ( "encoding/json" "testing" qt "github.com/frankban/quicktest" "gopkg.in/macaroon.v2" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/checkers" ) func TestNewMacaroon(t *testing.T) { c := qt.New(t) ns := checkers.NewNamespace(nil) m, err := bakery.NewMacaroon([]byte("rootkey"), []byte("some id"), "here", bakery.LatestVersion, ns) c.Assert(err, qt.IsNil) c.Assert(m.Namespace(), qt.Equals, ns) c.Assert(m.Version(), qt.Equals, bakery.LatestVersion) c.Assert(string(m.M().Id()), qt.Equals, "some id") c.Assert(m.M().Location(), qt.Equals, "here") c.Assert(m.M().Version(), qt.Equals, bakery.MacaroonVersion(bakery.LatestVersion)) } func TestAddFirstPartyCaveat(t *testing.T) { c := qt.New(t) ns := checkers.NewNamespace(nil) ns.Register("someuri", "x") m, err := bakery.NewMacaroon([]byte("rootkey"), []byte("some id"), "here", bakery.LatestVersion, ns) c.Assert(err, qt.IsNil) err = m.AddCaveat(testContext, checkers.Caveat{ Condition: "something", Namespace: "someuri", }, nil, nil) c.Assert(err, qt.IsNil) c.Assert(m.M().Caveats(), qt.DeepEquals, []macaroon.Caveat{{ Id: []byte("x:something"), }}) } // lbv holds the latest bakery version as used in the // third party caveat id. var lbv = byte(bakery.LatestVersion) var addThirdPartyCaveatTests = []struct { about string baseId []byte existingCaveatIds [][]byte expectId []byte }{{ about: "no existing id", expectId: []byte{lbv, 0}, }, { about: "several existing ids", existingCaveatIds: [][]byte{ {lbv, 0}, {lbv, 1}, {lbv, 2}, }, expectId: []byte{lbv, 3}, }, { about: "with base id", existingCaveatIds: [][]byte{ {lbv, 0}, }, baseId: []byte{lbv, 0}, expectId: []byte{lbv, 0, 0}, }, { about: "with base id and existing id", existingCaveatIds: [][]byte{ {lbv, 0, 0}, }, baseId: []byte{lbv, 0}, expectId: []byte{lbv, 0, 1}, }} func TestAddThirdPartyCaveat(t *testing.T) { c := qt.New(t) locator := bakery.NewThirdPartyStore() as := newBakery("as-loc", locator) for i, test := range addThirdPartyCaveatTests { c.Logf("test %d: %v", i, test.about) m, err := bakery.NewMacaroon([]byte("root key"), []byte("id"), "location", bakery.LatestVersion, nil) c.Assert(err, qt.IsNil) for _, id := range test.existingCaveatIds { err := m.M().AddThirdPartyCaveat(nil, id, "") c.Assert(err, qt.IsNil) } bakery.SetMacaroonCaveatIdPrefix(m, test.baseId) err = m.AddCaveat(testContext, checkers.Caveat{ Location: "as-loc", Condition: "something", }, as.Oven.Key(), locator) c.Assert(err, qt.IsNil) c.Assert(m.M().Caveats()[len(test.existingCaveatIds)].Id, qt.DeepEquals, test.expectId) } } func TestMarshalJSONLatestVersion(t *testing.T) { c := qt.New(t) locator := bakery.NewThirdPartyStore() as := newBakery("as-loc", locator) ns := checkers.NewNamespace(map[string]string{ "testns": "x", "otherns": "y", }) m, err := bakery.NewMacaroon([]byte("root key"), []byte("id"), "location", bakery.LatestVersion, ns) c.Assert(err, qt.IsNil) err = m.AddCaveat(testContext, checkers.Caveat{ Location: "as-loc", Condition: "something", }, as.Oven.Key(), locator) c.Assert(err, qt.IsNil) data, err := json.Marshal(m) c.Assert(err, qt.IsNil) var m1 *bakery.Macaroon err = json.Unmarshal(data, &m1) c.Assert(err, qt.IsNil) // Just check the signature and version - we're not interested in fully // checking the macaroon marshaling here. c.Assert(m1.M().Signature(), qt.DeepEquals, m.M().Signature()) c.Assert(m1.M().Version(), qt.Equals, m.M().Version()) c.Assert(m1.M().Caveats(), qt.HasLen, 1) c.Assert(m1.Namespace(), qt.DeepEquals, m.Namespace()) c.Assert(bakery.MacaroonCaveatData(m1), qt.DeepEquals, bakery.MacaroonCaveatData(m)) } func TestMarshalJSONVersion1(t *testing.T) { c := qt.New(t) testMarshalJSONWithVersion(c, bakery.Version1) } func TestMarshalJSONVersion2(t *testing.T) { c := qt.New(t) testMarshalJSONWithVersion(c, bakery.Version2) } func testMarshalJSONWithVersion(c *qt.C, version bakery.Version) { locator := bakery.NewThirdPartyStore() as := newBakery("as-loc", locator) ns := checkers.NewNamespace(map[string]string{ "testns": "x", }) m, err := bakery.NewMacaroon([]byte("root key"), []byte("id"), "location", version, ns) c.Assert(err, qt.IsNil) err = m.AddCaveat(testContext, checkers.Caveat{ Location: "as-loc", Condition: "something", }, as.Oven.Key(), locator) c.Assert(err, qt.IsNil) // Sanity check that no external caveat data has been added. c.Assert(bakery.MacaroonCaveatData(m), qt.HasLen, 0) data, err := json.Marshal(m) c.Assert(err, qt.IsNil) var m1 *bakery.Macaroon err = json.Unmarshal(data, &m1) c.Assert(err, qt.IsNil) // Just check the signature and version - we're not interested in fully // checking the macaroon marshaling here. c.Assert(m1.M().Signature(), qt.DeepEquals, m.M().Signature()) c.Assert(m1.M().Version(), qt.Equals, bakery.MacaroonVersion(version)) c.Assert(m1.M().Caveats(), qt.HasLen, 1) // Namespace information has been thrown away. c.Assert(m1.Namespace(), qt.DeepEquals, bakery.LegacyNamespace()) c.Assert(bakery.MacaroonCaveatData(m1), qt.HasLen, 0) // Check that we can unmarshal it directly as a V2 macaroon var m2 *macaroon.Macaroon err = json.Unmarshal(data, &m2) c.Assert(err, qt.IsNil) c.Assert(m2.Signature(), qt.DeepEquals, m.M().Signature()) c.Assert(m2.Version(), qt.Equals, bakery.MacaroonVersion(version)) c.Assert(m2.Caveats(), qt.HasLen, 1) } func TestUnmarshalJSONUnknownVersion(t *testing.T) { c := qt.New(t) m, err := macaroon.New(nil, nil, "", macaroon.V2) c.Assert(err, qt.IsNil) data, err := json.Marshal(bakery.MacaroonJSON{ Macaroon: m, Version: bakery.LatestVersion + 1, }) c.Assert(err, qt.IsNil) var m1 *bakery.Macaroon err = json.Unmarshal([]byte(data), &m1) c.Assert(err, qt.ErrorMatches, `unexpected bakery macaroon version; got 4 want 3`) } func TestUnmarshalJSONInconsistentVersion(t *testing.T) { c := qt.New(t) m, err := macaroon.New(nil, nil, "", macaroon.V1) c.Assert(err, qt.IsNil) data, err := json.Marshal(bakery.MacaroonJSON{ Macaroon: m, Version: bakery.LatestVersion, }) c.Assert(err, qt.IsNil) var m1 *bakery.Macaroon err = json.Unmarshal([]byte(data), &m1) c.Assert(err, qt.ErrorMatches, `underlying macaroon has inconsistent version; got 1 want 2`) } func TestClone(t *testing.T) { c := qt.New(t) locator := bakery.NewThirdPartyStore() as := newBakery("as-loc", locator) ns := checkers.NewNamespace(map[string]string{ "testns": "x", }) m, err := bakery.NewMacaroon([]byte("root key"), []byte("id"), "location", bakery.LatestVersion, ns) c.Assert(err, qt.IsNil) err = m.AddCaveat(testContext, checkers.Caveat{ Location: "as-loc", Condition: "something", }, as.Oven.Key(), locator) c.Assert(err, qt.IsNil) m1 := m.Clone() c.Assert(m.M().Caveats(), qt.HasLen, 1) c.Assert(m1.M().Caveats(), qt.HasLen, 1) c.Assert(bakery.MacaroonCaveatData(m), qt.DeepEquals, bakery.MacaroonCaveatData(m1)) err = m.AddCaveat(testContext, checkers.Caveat{ Location: "as-loc", Condition: "something", }, as.Oven.Key(), locator) c.Assert(err, qt.IsNil) c.Assert(m.M().Caveats(), qt.HasLen, 2) c.Assert(m1.M().Caveats(), qt.HasLen, 1) c.Assert(bakery.MacaroonCaveatData(m), qt.Not(qt.DeepEquals), bakery.MacaroonCaveatData(m1)) } func TestUnmarshalBadData(t *testing.T) { c := qt.New(t) var m1 *bakery.Macaroon err := json.Unmarshal([]byte(`{"m": []}`), &m1) c.Assert(err, qt.ErrorMatches, `json: cannot unmarshal array .*`) } macaroon-bakery-3.0.2/bakery/mgorootkeystore/000077500000000000000000000000001464427415500213335ustar00rootroot00000000000000macaroon-bakery-3.0.2/bakery/mgorootkeystore/export_test.go000066400000000000000000000001531464427415500242410ustar00rootroot00000000000000package mgorootkeystore var ( Clock = &clock MgoCollectionFindId = &mgoCollectionFindId ) macaroon-bakery-3.0.2/bakery/mgorootkeystore/rootkey.go000066400000000000000000000157201464427415500233630ustar00rootroot00000000000000// Package mgorootkeystore provides an implementation of bakery.RootKeyStore // that uses MongoDB as a persistent store. package mgorootkeystore import ( "context" "time" "github.com/juju/mgo/v2" "github.com/juju/mgo/v2/bson" "gopkg.in/errgo.v1" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/dbrootkeystore" ) // Functions defined as variables so they can be overidden // for testing. var ( clock dbrootkeystore.Clock mgoCollectionFindId = (*mgo.Collection).FindId ) // TODO it would be nice if could make Policy // a type alias for dbrootkeystore.Policy, // but we want to be able to support versions // of Go from before type aliases were introduced. // Policy holds a store policy for root keys. type Policy dbrootkeystore.Policy // maxPolicyCache holds the maximum number of store policies that can // hold cached keys in a given RootKeys instance. // // 100 is probably overkill, given that practical systems will // likely only have a small number of active policies on any given // macaroon collection. const maxPolicyCache = 100 // RootKeys represents a cache of macaroon root keys. type RootKeys struct { keys *dbrootkeystore.RootKeys } // NewRootKeys returns a root-keys cache that // is limited in size to approximately the given size. // // The NewStore method returns a store implementation // that uses a specific mongo collection and store // policy. func NewRootKeys(maxCacheSize int) *RootKeys { return &RootKeys{ keys: dbrootkeystore.NewRootKeys(maxCacheSize, clock), } } // NewStore returns a new RootKeyStore implementation that // stores and obtains root keys from the given collection. // // Root keys will be generated and stored following the // given store policy. // // It is expected that all collections passed to a given Store's // NewStore method should refer to the same underlying collection. func (s *RootKeys) NewStore(c *mgo.Collection, policy Policy) bakery.RootKeyStore { return s.keys.NewStore(backing{c}, dbrootkeystore.Policy(policy)) } var indexes = []mgo.Index{{ Key: []string{"-created"}, }, { Key: []string{"expires"}, ExpireAfter: time.Second, }} // EnsureIndex ensures that the required indexes exist on the // collection that will be used for root key store. // This should be called at least once before using NewStore. func (s *RootKeys) EnsureIndex(c *mgo.Collection) error { for _, idx := range indexes { if err := c.EnsureIndex(idx); err != nil { return errgo.Notef(err, "cannot ensure index for %q on %q", idx.Key, c.Name) } } return nil } type backing struct { coll *mgo.Collection } var _ dbrootkeystore.Backing = backing{} var _ dbrootkeystore.ContextBacking = backing{} // GetKey implements dbrootkeystore.Backing. func (b backing) GetKey(id []byte) (dbrootkeystore.RootKey, error) { return getFromMongo(b.coll, id) } // GetKeyContext implements dbrootkeystore.ContextBacking. func (b backing) GetKeyContext(ctx context.Context, id []byte) (dbrootkeystore.RootKey, error) { var rk dbrootkeystore.RootKey var err error f := func(coll *mgo.Collection) { rk, err = getFromMongo(coll, id) } if err := b.runWithContext(ctx, f); err != nil { return dbrootkeystore.RootKey{}, err } return rk, err } func getFromMongo(coll *mgo.Collection, id []byte) (dbrootkeystore.RootKey, error) { var key dbrootkeystore.RootKey err := mgoCollectionFindId(coll, id).One(&key) if err != nil { if err == mgo.ErrNotFound { return getLegacyFromMongo(coll, string(id)) } return dbrootkeystore.RootKey{}, errgo.Notef(err, "cannot get key from database") } // TODO migrate the key from the old format to the new format. return key, nil } // getLegacyFromMongo gets a value from the old version of the // root key document which used a string key rather than a []byte // key. func getLegacyFromMongo(coll *mgo.Collection, id string) (dbrootkeystore.RootKey, error) { var key dbrootkeystore.RootKey err := mgoCollectionFindId(coll, id).One(&key) if err != nil { if err == mgo.ErrNotFound { return dbrootkeystore.RootKey{}, bakery.ErrNotFound } return dbrootkeystore.RootKey{}, errgo.Notef(err, "cannot get key from database") } return key, nil } // FindLatestKey implements dbrootkeystore.Backing. func (b backing) FindLatestKey(createdAfter, expiresAfter, expiresBefore time.Time) (dbrootkeystore.RootKey, error) { return findLatestKey(b.coll, createdAfter, expiresAfter, expiresBefore) } // FindLatestKeyContext implements dbrootkeystore.ContextBacking. func (b backing) FindLatestKeyContext(ctx context.Context, createdAfter, expiresAfter, expiresBefore time.Time) (dbrootkeystore.RootKey, error) { var rk dbrootkeystore.RootKey var err error f := func(coll *mgo.Collection) { rk, err = findLatestKey(coll, createdAfter, expiresAfter, expiresBefore) } if err := b.runWithContext(ctx, f); err != nil { return dbrootkeystore.RootKey{}, err } return rk, err } func findLatestKey(coll *mgo.Collection, createdAfter, expiresAfter, expiresBefore time.Time) (dbrootkeystore.RootKey, error) { var key dbrootkeystore.RootKey err := coll.Find(bson.D{{ "created", bson.D{{"$gte", createdAfter}}, }, { "expires", bson.D{ {"$gte", expiresAfter}, {"$lte", expiresBefore}, }, }}).Sort("-created").One(&key) if err != nil && err != mgo.ErrNotFound { return dbrootkeystore.RootKey{}, errgo.Notef(err, "cannot query existing keys") } return key, nil } // InsertKey implements dbrootkeystore.Backing. func (b backing) InsertKey(key dbrootkeystore.RootKey) error { return insertKey(b.coll, key) } // InsertKeyContext implements dbrootkeystore.ContextBacking. func (b backing) InsertKeyContext(ctx context.Context, key dbrootkeystore.RootKey) error { var err error f := func(coll *mgo.Collection) { err = insertKey(coll, key) } if err := b.runWithContext(ctx, f); err != nil { return err } return err } func insertKey(coll *mgo.Collection, key dbrootkeystore.RootKey) error { if err := coll.Insert(key); err != nil { return errgo.Notef(err, "mongo insert failed") } return nil } func (b backing) runWithContext(ctx context.Context, f func(*mgo.Collection)) error { s := sessionFromContext(ctx) if s == nil { s = b.coll.Database.Session } s = s.Clone() c := make(chan struct{}) go func() { defer close(c) defer s.Close() f(b.coll.With(s)) }() select { case <-ctx.Done(): return ctx.Err() case <-c: return nil } } type contextSessionKey struct{} // ContextWithMgoSession adds the given mgo.Session to the given // context.Context. Any operations requiring database access that are // made using a context with an attached session will use the session // from the context to access mongodb, rather than the session in the // collection used when the RootKeyStore was created. func ContextWithMgoSession(ctx context.Context, s *mgo.Session) context.Context { return context.WithValue(ctx, contextSessionKey{}, s) } func sessionFromContext(ctx context.Context) *mgo.Session { s, _ := ctx.Value(contextSessionKey{}).(*mgo.Session) return s } macaroon-bakery-3.0.2/bakery/mgorootkeystore/rootkey_test.go000066400000000000000000000372371464427415500244310ustar00rootroot00000000000000package mgorootkeystore_test import ( "context" "fmt" "testing" "time" qt "github.com/frankban/quicktest" "github.com/juju/mgo/v2" "github.com/juju/mgotest" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/dbrootkeystore" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/mgorootkeystore" ) var epoch = time.Date(2000, time.January, 1, 0, 0, 0, 0, time.UTC) var isValidWithPolicyTests = []struct { about string policy mgorootkeystore.Policy now time.Time key dbrootkeystore.RootKey expect bool }{{ about: "success", policy: mgorootkeystore.Policy{ GenerateInterval: 2 * time.Minute, ExpiryDuration: 3 * time.Minute, }, now: epoch.Add(20 * time.Minute), key: dbrootkeystore.RootKey{ Created: epoch.Add(19 * time.Minute), Expires: epoch.Add(24 * time.Minute), Id: []byte("id"), RootKey: []byte("key"), }, expect: true, }, { about: "empty root key", policy: mgorootkeystore.Policy{ GenerateInterval: 2 * time.Minute, ExpiryDuration: 3 * time.Minute, }, now: epoch.Add(20 * time.Minute), key: dbrootkeystore.RootKey{}, expect: false, }, { about: "created too early", policy: mgorootkeystore.Policy{ GenerateInterval: 2 * time.Minute, ExpiryDuration: 3 * time.Minute, }, now: epoch.Add(20 * time.Minute), key: dbrootkeystore.RootKey{ Created: epoch.Add(18*time.Minute - time.Millisecond), Expires: epoch.Add(24 * time.Minute), Id: []byte("id"), RootKey: []byte("key"), }, expect: false, }, { about: "expires too early", policy: mgorootkeystore.Policy{ GenerateInterval: 2 * time.Minute, ExpiryDuration: 3 * time.Minute, }, now: epoch.Add(20 * time.Minute), key: dbrootkeystore.RootKey{ Created: epoch.Add(19 * time.Minute), Expires: epoch.Add(21 * time.Minute), Id: []byte("id"), RootKey: []byte("key"), }, expect: false, }, { about: "expires too late", policy: mgorootkeystore.Policy{ GenerateInterval: 2 * time.Minute, ExpiryDuration: 3 * time.Minute, }, now: epoch.Add(20 * time.Minute), key: dbrootkeystore.RootKey{ Created: epoch.Add(19 * time.Minute), Expires: epoch.Add(25*time.Minute + time.Millisecond), Id: []byte("id"), RootKey: []byte("key"), }, expect: false, }} func TestIsValidWithPolicy(t *testing.T) { c := qt.New(t) var now time.Time c.Patch(mgorootkeystore.Clock, clockVal(&now)) for i, test := range isValidWithPolicyTests { c.Logf("test %d: %v", i, test.about) c.Assert(test.key.IsValidWithPolicy(dbrootkeystore.Policy(test.policy), test.now), qt.Equals, test.expect) } } func TestRootKeyUsesKeysValidWithPolicy(t *testing.T) { c := qt.New(t) // We re-use the TestIsValidWithPolicy tests so that we // know that the mongo logic uses the same behaviour. var now time.Time c.Patch(mgorootkeystore.Clock, clockVal(&now)) for _, test := range isValidWithPolicyTests { c.Run(test.about, func(c *qt.C) { if test.key.RootKey == nil { // We don't store empty root keys in the database. c.Skip("skipping test with empty root key") } coll := testColl(c) // Prime the collection with the root key document. err := coll.Insert(test.key) c.Assert(err, qt.IsNil) store := mgorootkeystore.NewRootKeys(10).NewStore(coll, test.policy) now = test.now key, id, err := store.RootKey(context.Background()) c.Assert(err, qt.IsNil) if test.expect { c.Assert(string(id), qt.Equals, "id") c.Assert(string(key), qt.Equals, "key") } else { // If it didn't match then RootKey will have // generated a new key. c.Assert(key, qt.HasLen, 24) c.Assert(id, qt.HasLen, 32) } }) } } func TestRootKey(t *testing.T) { c := qt.New(t) defer c.Done() now := epoch c.Patch(mgorootkeystore.Clock, clockVal(&now)) coll := testColl(c) store := mgorootkeystore.NewRootKeys(10).NewStore(coll, mgorootkeystore.Policy{ GenerateInterval: 2 * time.Minute, ExpiryDuration: 5 * time.Minute, }) key, id, err := store.RootKey(context.Background()) c.Assert(err, qt.IsNil) c.Assert(key, qt.HasLen, 24) c.Assert(id, qt.HasLen, 32) // If we get a key within the generate interval, we should // get the same one. now = epoch.Add(time.Minute) key1, id1, err := store.RootKey(context.Background()) c.Assert(err, qt.IsNil) c.Assert(key1, qt.DeepEquals, key) c.Assert(id1, qt.DeepEquals, id) // A different store instance should get the same root key. store1 := mgorootkeystore.NewRootKeys(10).NewStore(coll, mgorootkeystore.Policy{ GenerateInterval: 2 * time.Minute, ExpiryDuration: 5 * time.Minute, }) key1, id1, err = store1.RootKey(context.Background()) c.Assert(err, qt.IsNil) c.Assert(key1, qt.DeepEquals, key) c.Assert(id1, qt.DeepEquals, id) // After the generation interval has passed, we should generate a new key. now = epoch.Add(2*time.Minute + time.Second) key1, id1, err = store.RootKey(context.Background()) c.Assert(err, qt.IsNil) c.Assert(key, qt.HasLen, 24) c.Assert(id, qt.HasLen, 32) c.Assert(key1, qt.Not(qt.DeepEquals), key) c.Assert(id1, qt.Not(qt.DeepEquals), id) // The other store should pick it up too. key2, id2, err := store1.RootKey(context.Background()) c.Assert(err, qt.IsNil) c.Assert(key2, qt.DeepEquals, key1) c.Assert(id2, qt.DeepEquals, id1) } func TestRootKeyDefaultGenerateInterval(t *testing.T) { c := qt.New(t) defer c.Done() now := epoch c.Patch(mgorootkeystore.Clock, clockVal(&now)) coll := testColl(c) store := mgorootkeystore.NewRootKeys(10).NewStore(coll, mgorootkeystore.Policy{ ExpiryDuration: 5 * time.Minute, }) key, id, err := store.RootKey(context.Background()) c.Assert(err, qt.IsNil) now = epoch.Add(5 * time.Minute) key1, id1, err := store.RootKey(context.Background()) c.Assert(err, qt.IsNil) c.Assert(key1, qt.DeepEquals, key) c.Assert(id1, qt.DeepEquals, id) now = epoch.Add(5*time.Minute + time.Millisecond) key1, id1, err = store.RootKey(context.Background()) c.Assert(err, qt.IsNil) c.Assert(string(key1), qt.Not(qt.Equals), string(key)) c.Assert(string(id1), qt.Not(qt.Equals), string(id)) } var preferredRootKeyTests = []struct { about string now time.Time keys []dbrootkeystore.RootKey policy mgorootkeystore.Policy expectId []byte }{{ about: "latest creation time is preferred", now: epoch.Add(5 * time.Minute), keys: []dbrootkeystore.RootKey{{ Created: epoch.Add(4 * time.Minute), Expires: epoch.Add(15 * time.Minute), Id: []byte("id0"), RootKey: []byte("key0"), }, { Created: epoch.Add(5*time.Minute + 30*time.Second), Expires: epoch.Add(16 * time.Minute), Id: []byte("id1"), RootKey: []byte("key1"), }, { Created: epoch.Add(5 * time.Minute), Expires: epoch.Add(16 * time.Minute), Id: []byte("id2"), RootKey: []byte("key2"), }}, policy: mgorootkeystore.Policy{ GenerateInterval: 5 * time.Minute, ExpiryDuration: 7 * time.Minute, }, expectId: []byte("id1"), }, { about: "ineligible keys are exluded", now: epoch.Add(5 * time.Minute), keys: []dbrootkeystore.RootKey{{ Created: epoch.Add(4 * time.Minute), Expires: epoch.Add(15 * time.Minute), Id: []byte("id0"), RootKey: []byte("key0"), }, { Created: epoch.Add(5 * time.Minute), Expires: epoch.Add(16*time.Minute + 30*time.Second), Id: []byte("id1"), RootKey: []byte("key1"), }, { Created: epoch.Add(6 * time.Minute), Expires: epoch.Add(time.Hour), Id: []byte("id2"), RootKey: []byte("key2"), }}, policy: mgorootkeystore.Policy{ GenerateInterval: 5 * time.Minute, ExpiryDuration: 7 * time.Minute, }, expectId: []byte("id1"), }} func TestPreferredRootKeyFromDatabase(t *testing.T) { c := qt.New(t) defer c.Done() var now time.Time c.Patch(mgorootkeystore.Clock, clockVal(&now)) for _, test := range preferredRootKeyTests { c.Run(test.about, func(c *qt.C) { coll := testColl(c) for _, key := range test.keys { err := coll.Insert(key) c.Assert(err, qt.IsNil) } store := mgorootkeystore.NewRootKeys(10).NewStore(coll, test.policy) now = test.now _, id, err := store.RootKey(context.Background()) c.Assert(err, qt.IsNil) c.Assert(id, qt.DeepEquals, test.expectId) }) } } func TestPreferredRootKeyFromCache(t *testing.T) { c := qt.New(t) defer c.Done() var now time.Time c.Patch(mgorootkeystore.Clock, clockVal(&now)) for _, test := range preferredRootKeyTests { c.Run(test.about, func(c *qt.C) { coll := testColl(c) for _, key := range test.keys { err := coll.Insert(key) c.Assert(err, qt.IsNil) } store := mgorootkeystore.NewRootKeys(10).NewStore(coll, test.policy) // Ensure that all the keys are in cache by getting all of them. for _, key := range test.keys { got, err := store.Get(context.Background(), key.Id) c.Assert(err, qt.IsNil) c.Assert(got, qt.DeepEquals, key.RootKey) } // Remove all the keys from the collection so that // we know we must be acquiring them from the cache. _, err := coll.RemoveAll(nil) c.Assert(err, qt.IsNil) // Test that RootKey returns the expected key. now = test.now _, id, err := store.RootKey(context.Background()) c.Assert(err, qt.IsNil) c.Assert(id, qt.DeepEquals, test.expectId) }) } } func TestGet(t *testing.T) { c := qt.New(t) defer c.Done() now := epoch c.Patch(mgorootkeystore.Clock, clockVal(&now)) coll := testColl(c) store := mgorootkeystore.NewRootKeys(5).NewStore(coll, mgorootkeystore.Policy{ GenerateInterval: 1 * time.Minute, ExpiryDuration: 30 * time.Minute, }) type idKey struct { id string key []byte } var keys []idKey keyIds := make(map[string]bool) for i := 0; i < 20; i++ { key, id, err := store.RootKey(context.Background()) c.Assert(err, qt.IsNil) c.Assert(keyIds[string(id)], qt.Equals, false) keys = append(keys, idKey{string(id), key}) now = now.Add(time.Minute + time.Second) } for i, k := range keys { key, err := store.Get(context.Background(), []byte(k.id)) c.Assert(err, qt.IsNil, qt.Commentf("key %d (%s)", i, k.id)) c.Assert(key, qt.DeepEquals, k.key, qt.Commentf("key %d (%s)", i, k.id)) } // Check that the keys are cached. // // Since the cache size is 5, the most recent 5 items will be in // the primary cache; the 5 items before that will be in the old // cache and nothing else will be cached. // // The first time we fetch an item from the old cache, a new // primary cache will be allocated, all existing items in the // old cache except that item will be evicted, and all items in // the current primary cache moved to the old cache. // // The upshot of that is that all but the first 6 calls to Get // should result in a database fetch. var fetched []string c.Patch(mgorootkeystore.MgoCollectionFindId, func(coll *mgo.Collection, id interface{}) *mgo.Query { fetched = append(fetched, string(id.([]byte))) return coll.FindId(id) }) c.Logf("testing cache") for i := len(keys) - 1; i >= 0; i-- { k := keys[i] key, err := store.Get(context.Background(), []byte(k.id)) c.Assert(err, qt.IsNil) c.Assert(err, qt.IsNil, qt.Commentf("key %d (%s)", i, k.id)) c.Assert(key, qt.DeepEquals, k.key, qt.Commentf("key %d (%s)", i, k.id)) } c.Assert(len(fetched), qt.Equals, len(keys)-6) for i, id := range fetched { c.Assert(id, qt.Equals, keys[len(keys)-6-i-1].id) } } func TestGetCachesMisses(t *testing.T) { c := qt.New(t) defer c.Done() coll := testColl(c) store := mgorootkeystore.NewRootKeys(5).NewStore(coll, mgorootkeystore.Policy{ GenerateInterval: 1 * time.Minute, ExpiryDuration: 30 * time.Minute, }) var fetched []string c.Patch(mgorootkeystore.MgoCollectionFindId, func(coll *mgo.Collection, id interface{}) *mgo.Query { fetched = append(fetched, fmt.Sprintf("%#v", id)) return coll.FindId(id) }) key, err := store.Get(context.Background(), []byte("foo")) c.Assert(err, qt.Equals, bakery.ErrNotFound) c.Assert(key, qt.IsNil) // This should check twice first using a []byte second using a string c.Assert(fetched, qt.DeepEquals, []string{fmt.Sprintf("%#v", []byte("foo")), fmt.Sprintf("%#v", "foo")}) fetched = nil key, err = store.Get(context.Background(), []byte("foo")) c.Assert(err, qt.Equals, bakery.ErrNotFound) c.Assert(key, qt.IsNil) c.Assert(fetched, qt.IsNil) } func TestGetExpiredItemFromCache(t *testing.T) { c := qt.New(t) defer c.Done() now := epoch c.Patch(mgorootkeystore.Clock, clockVal(&now)) coll := testColl(c) store := mgorootkeystore.NewRootKeys(10).NewStore(coll, mgorootkeystore.Policy{ ExpiryDuration: 5 * time.Minute, }) _, id, err := store.RootKey(context.Background()) c.Assert(err, qt.IsNil) c.Patch(mgorootkeystore.MgoCollectionFindId, func(*mgo.Collection, interface{}) *mgo.Query { c.Errorf("FindId unexpectedly called") return nil }) now = epoch.Add(15 * time.Minute) _, err = store.Get(context.Background(), id) c.Assert(err, qt.Equals, bakery.ErrNotFound) } func TestEnsureIndex(t *testing.T) { c := qt.New(t) defer c.Done() keys := mgorootkeystore.NewRootKeys(5) coll := testColl(c) err := keys.EnsureIndex(coll) c.Assert(err, qt.IsNil) // This code can take up to 60s to run; there's no way // to force it to run more quickly, but it provides reassurance // that the code actually works. // Reenable the rest of this test if concerned about index behaviour. c.Skip("test runs too slowly") _, id1, err := keys.NewStore(coll, mgorootkeystore.Policy{ ExpiryDuration: 100 * time.Millisecond, }).RootKey(context.Background()) c.Assert(err, qt.IsNil) _, id2, err := keys.NewStore(coll, mgorootkeystore.Policy{ ExpiryDuration: time.Hour, }).RootKey(context.Background()) c.Assert(err, qt.IsNil) c.Assert(id2, qt.Not(qt.Equals), id1) // Sanity check that the keys are in the collection. n, err := coll.Find(nil).Count() c.Assert(err, qt.IsNil) c.Assert(n, qt.Equals, 2) for i := 0; i < 100; i++ { n, err := coll.Find(nil).Count() c.Assert(err, qt.IsNil) switch n { case 1: return case 2: time.Sleep(time.Second) default: c.Fatalf("unexpected key count %v", n) } } c.Fatalf("key was never removed from database") } type legacyRootKey struct { Id string `bson:"_id"` Created time.Time Expires time.Time RootKey []byte } func TestLegacy(t *testing.T) { c := qt.New(t) defer c.Done() coll := testColl(c) err := coll.Insert(&legacyRootKey{ Id: "foo", RootKey: []byte("a key"), Created: time.Now(), Expires: time.Now().Add(10 * time.Minute), }) c.Assert(err, qt.IsNil) store := mgorootkeystore.NewRootKeys(10).NewStore(coll, mgorootkeystore.Policy{ ExpiryDuration: 5 * time.Minute, }) rk, err := store.Get(context.Background(), []byte("foo")) c.Assert(err, qt.IsNil) c.Assert(string(rk), qt.Equals, "a key") } func TestUsesSessionFromContext(t *testing.T) { c := qt.New(t) defer c.Done() coll := testColl(c) s1 := coll.Database.Session.Copy() s2 := coll.Database.Session.Copy() c.Defer(s2.Close) coll = coll.With(s1) store := mgorootkeystore.NewRootKeys(10).NewStore(coll, mgorootkeystore.Policy{ ExpiryDuration: 5 * time.Minute, }) s1.Close() ctx := mgorootkeystore.ContextWithMgoSession(context.Background(), s2) _, _, err := store.RootKey(ctx) c.Assert(err, qt.Equals, nil) } func TestDoneContext(t *testing.T) { c := qt.New(t) defer c.Done() store := mgorootkeystore.NewRootKeys(10).NewStore(testColl(c), mgorootkeystore.Policy{ ExpiryDuration: 5 * time.Minute, }) ctx, cancel := context.WithCancel(context.Background()) cancel() _, _, err := store.RootKey(ctx) c.Assert(err, qt.ErrorMatches, `cannot query existing keys: context canceled`) } func testColl(c *qt.C) *mgo.Collection { db, err := mgotest.New() c.Assert(err, qt.Equals, nil) c.Defer(func() { err := db.Close() c.Check(err, qt.Equals, nil) }) return db.C("rootkeyitems") } func clockVal(t *time.Time) dbrootkeystore.Clock { return clockFunc(func() time.Time { return *t }) } type clockFunc func() time.Time func (f clockFunc) Now() time.Time { return f() } macaroon-bakery-3.0.2/bakery/oven.go000066400000000000000000000250561464427415500173650ustar00rootroot00000000000000package bakery import ( "bytes" "context" "encoding/base64" "sort" "github.com/go-macaroon-bakery/macaroonpb" "github.com/rogpeppe/fastuuid" "gopkg.in/errgo.v1" "gopkg.in/macaroon.v2" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/checkers" ) // MacaroonVerifier verifies macaroons and returns the operations and // caveats they're associated with. type MacaroonVerifier interface { // VerifyMacaroon verifies the signature of the given macaroon and returns // information on its associated operations, and all the first party // caveat conditions that need to be checked. // // This method should not check first party caveats itself. // // It should return a *VerificationError if the error occurred // because the macaroon signature failed or the root key // was not found - any other error will be treated as fatal // by Checker and cause authorization to terminate. VerifyMacaroon(ctx context.Context, ms macaroon.Slice) ([]Op, []string, error) } var uuidGen = fastuuid.MustNewGenerator() // Oven bakes macaroons. They emerge sweet and delicious // and ready for use in a Checker. // // All macaroons are associated with one or more operations (see // the Op type) which define the capabilities of the macaroon. // // There is one special operation, "login" (defined by LoginOp) // which grants the capability to speak for a particular user. // The login capability will never be mixed with other capabilities. // // It is up to the caller to decide on semantics for other operations. type Oven struct { p OvenParams } type OvenParams struct { // Namespace holds the namespace to use when adding first party caveats. // If this is nil, checkers.New(nil).Namespace will be used. Namespace *checkers.Namespace // RootKeyStoreForEntity returns the macaroon storage to be // used for root keys associated with macaroons created // wth NewMacaroon. // // If this is nil, NewMemRootKeyStore will be used to create // a new store to be used for all entities. RootKeyStoreForOps func(ops []Op) RootKeyStore // Key holds the private key pair used to encrypt third party caveats. // If it is nil, no third party caveats can be created. Key *KeyPair // Location holds the location that will be associated with new macaroons // (as returned by Macaroon.Location). Location string // Locator is used to find out information on third parties when // adding third party caveats. If this is nil, no non-local third // party caveats can be added. Locator ThirdPartyLocator // LegacyMacaroonOp holds the operation to associate with old // macaroons that don't have associated operations. // If this is empty, legacy macaroons will not be associated // with any operations. LegacyMacaroonOp Op // TODO max macaroon or macaroon id size? } // NewOven returns a new oven using the given parameters. func NewOven(p OvenParams) *Oven { if p.Locator == nil { p.Locator = emptyLocator{} } if p.RootKeyStoreForOps == nil { store := NewMemRootKeyStore() p.RootKeyStoreForOps = func(ops []Op) RootKeyStore { return store } } if p.Namespace == nil { p.Namespace = checkers.New(nil).Namespace() } return &Oven{ p: p, } } // VerifyMacaroon implements MacaroonVerifier.VerifyMacaroon, making Oven // an instance of MacaroonVerifier. // // For macaroons minted with previous bakery versions, it always // returns a single LoginOp operation. func (o *Oven) VerifyMacaroon(ctx context.Context, ms macaroon.Slice) (ops []Op, conditions []string, err error) { if len(ms) == 0 { return nil, nil, errgo.Newf("no macaroons in slice") } storageId, ops, err := o.decodeMacaroonId(ms[0].Id()) if err != nil { return nil, nil, errgo.Mask(err) } rootKey, err := o.p.RootKeyStoreForOps(ops).Get(ctx, storageId) if err != nil { if errgo.Cause(err) != ErrNotFound { return nil, nil, errgo.Notef(err, "cannot get macaroon") } // If the macaroon was not found, it is probably // because it's been removed after time-expiry, // so return a verification error. return nil, nil, &VerificationError{ Reason: errgo.Newf("macaroon not found in storage"), } } conditions, err = ms[0].VerifySignature(rootKey, ms[1:]) if err != nil { return nil, nil, &VerificationError{ Reason: errgo.Mask(err), } } return ops, conditions, nil } func (o *Oven) decodeMacaroonId(id []byte) (storageId []byte, ops []Op, err error) { base64Decoded := false if id[0] == 'A' { // The first byte is not a version number and it's 'A', which is the // base64 encoding of the top 6 bits (all zero) of the version number 2 or 3, // so we assume that it's the base64 encoding of a new-style // macaroon id, so we base64 decode it. // // Note that old-style ids always start with an ASCII character >= 4 // (> 32 in fact) so this logic won't be triggered for those. dec := make([]byte, base64.RawURLEncoding.DecodedLen(len(id))) n, err := base64.RawURLEncoding.Decode(dec, id) if err == nil { // Set the id only on success - if it's a bad encoding, we'll get a not-found error // which is fine because "not found" is a correct description of the issue - we // can't find the root key for the given id. id = dec[0:n] base64Decoded = true } } // Trim any extraneous information from the id before retrieving // it from storage, including the UUID that's added when // creating macaroons to make all macaroons unique even if // they're using the same root key. switch id[0] { case byte(Version2): // Skip the UUID at the start of the id. storageId = id[1+16:] case byte(Version3): var id1 macaroonpb.MacaroonId if err := id1.UnmarshalBinary(id[1:]); err != nil { return nil, nil, errgo.Notef(err, "cannot unmarshal macaroon id") } if len(id1.Ops) == 0 || len(id1.Ops[0].Actions) == 0 { return nil, nil, errgo.Newf("no operations found in macaroon") } ops = make([]Op, 0, len(id1.Ops)) for _, op := range id1.Ops { for _, action := range op.Actions { ops = append(ops, Op{ Entity: op.Entity, Action: action, }) } } return id1.StorageId, ops, nil } if !base64Decoded && isLowerCaseHexChar(id[0]) { // It's an old-style id, probably with a hyphenated UUID. // so trim that off. if i := bytes.LastIndexByte(id, '-'); i >= 0 { storageId = id[0:i] } } if op := o.p.LegacyMacaroonOp; op != (Op{}) { ops = []Op{op} } return storageId, ops, nil } // NewMacaroon takes a macaroon with the given version from the oven, associates it with the given operations // and attaches the given caveats. There must be at least one operation specified. func (o *Oven) NewMacaroon(ctx context.Context, version Version, caveats []checkers.Caveat, ops ...Op) (*Macaroon, error) { if len(ops) == 0 { return nil, errgo.Newf("cannot mint a macaroon associated with no operations") } ops = CanonicalOps(ops) rootKey, storageId, err := o.p.RootKeyStoreForOps(ops).RootKey(ctx) if err != nil { return nil, errgo.Mask(err) } id, err := o.newMacaroonId(ctx, ops, storageId) if err != nil { return nil, errgo.Mask(err) } idBytesNoVersion, err := id.MarshalBinary() if err != nil { return nil, errgo.Mask(err) } idBytes := make([]byte, len(idBytesNoVersion)+1) idBytes[0] = byte(LatestVersion) // TODO We could use a proto.Buffer to avoid this copy. copy(idBytes[1:], idBytesNoVersion) if MacaroonVersion(version) < macaroon.V2 { // The old macaroon format required valid text for the macaroon id, // so base64-encode it. b64data := make([]byte, base64.RawURLEncoding.EncodedLen(len(idBytes))) base64.RawURLEncoding.Encode(b64data, idBytes) idBytes = b64data } m, err := NewMacaroon(rootKey, idBytes, o.p.Location, version, o.p.Namespace) if err != nil { return nil, errgo.Notef(err, "cannot create macaroon with version %v", version) } if err := o.AddCaveats(ctx, m, caveats); err != nil { return nil, errgo.Mask(err) } return m, nil } // AddCaveat adds a caveat to the given macaroon. func (o *Oven) AddCaveat(ctx context.Context, m *Macaroon, cav checkers.Caveat) error { return m.AddCaveat(ctx, cav, o.p.Key, o.p.Locator) } // AddCaveats adds all the caveats to the given macaroon. func (o *Oven) AddCaveats(ctx context.Context, m *Macaroon, caveats []checkers.Caveat) error { return m.AddCaveats(ctx, caveats, o.p.Key, o.p.Locator) } // Key returns the oven's private/public key par. func (o *Oven) Key() *KeyPair { return o.p.Key } // Locator returns the third party locator that the // oven was created with. func (o *Oven) Locator() ThirdPartyLocator { return o.p.Locator } // CanonicalOps returns the given operations slice sorted // with duplicates removed. func CanonicalOps(ops []Op) []Op { canonOps := opsByValue(ops) needNewSlice := false for i := 1; i < len(ops); i++ { if !canonOps.Less(i-1, i) { needNewSlice = true break } } if !needNewSlice { return ops } canonOps = make([]Op, len(ops)) copy(canonOps, ops) sort.Sort(canonOps) // Note we know that there's at least one operation here // because we'd have returned earlier if the slice was empty. j := 0 for _, op := range canonOps[1:] { if op != canonOps[j] { j++ canonOps[j] = op } } return canonOps[0 : j+1] } func (o *Oven) newMacaroonId(ctx context.Context, ops []Op, storageId []byte) (*macaroonpb.MacaroonId, error) { uuid := uuidGen.Next() nonce := uuid[0:16] return &macaroonpb.MacaroonId{ Nonce: nonce, StorageId: storageId, Ops: macaroonIdOps(ops), }, nil } // macaroonIdOps returns operations suitable for serializing // as part of an *macaroonpb.MacaroonId. It assumes that // ops has been canonicalized and that there's at least // one operation. func macaroonIdOps(ops []Op) []*macaroonpb.Op { idOps := make([]macaroonpb.Op, 0, len(ops)) idOps = append(idOps, macaroonpb.Op{ Entity: ops[0].Entity, Actions: []string{ops[0].Action}, }) i := 0 idOp := &idOps[0] for _, op := range ops[1:] { if op.Entity != idOp.Entity { idOps = append(idOps, macaroonpb.Op{ Entity: op.Entity, Actions: []string{op.Action}, }) i++ idOp = &idOps[i] continue } if op.Action != idOp.Actions[len(idOp.Actions)-1] { idOp.Actions = append(idOp.Actions, op.Action) } } idOpPtrs := make([]*macaroonpb.Op, len(idOps)) for i := range idOps { idOpPtrs[i] = &idOps[i] } return idOpPtrs } type opsByValue []Op func (o opsByValue) Less(i, j int) bool { o0, o1 := o[i], o[j] if o0.Entity != o1.Entity { return o0.Entity < o1.Entity } return o0.Action < o1.Action } func (o opsByValue) Swap(i, j int) { o[i], o[j] = o[j], o[i] } func (o opsByValue) Len() int { return len(o) } func isLowerCaseHexChar(c byte) bool { switch { case '0' <= c && c <= '9': return true case 'a' <= c && c <= 'f': return true } return false } macaroon-bakery-3.0.2/bakery/oven_test.go000066400000000000000000000056171464427415500204250ustar00rootroot00000000000000package bakery_test import ( "testing" qt "github.com/frankban/quicktest" "gopkg.in/macaroon.v2" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" ) var canonicalOpsTests = []struct { about string ops []bakery.Op expect []bakery.Op }{{ about: "empty slice", }, { about: "one element", ops: []bakery.Op{{"a", "a"}}, expect: []bakery.Op{{"a", "a"}}, }, { about: "all in order", ops: []bakery.Op{{"a", "a"}, {"a", "b"}, {"c", "c"}}, expect: []bakery.Op{{"a", "a"}, {"a", "b"}, {"c", "c"}}, }, { about: "out of order", ops: []bakery.Op{{"c", "c"}, {"a", "b"}, {"a", "a"}}, expect: []bakery.Op{{"a", "a"}, {"a", "b"}, {"c", "c"}}, }, { about: "with duplicates", ops: []bakery.Op{{"c", "c"}, {"a", "b"}, {"a", "a"}, {"c", "a"}, {"c", "b"}, {"c", "c"}, {"a", "a"}}, expect: []bakery.Op{{"a", "a"}, {"a", "b"}, {"c", "a"}, {"c", "b"}, {"c", "c"}}, }, { about: "make sure we've got the fields right", ops: []bakery.Op{{Entity: "read", Action: "two"}, {Entity: "read", Action: "one"}, {Entity: "write", Action: "one"}}, expect: []bakery.Op{{Entity: "read", Action: "one"}, {Entity: "read", Action: "two"}, {Entity: "write", Action: "one"}}, }} func TestCanonicalOps(t *testing.T) { c := qt.New(t) for i, test := range canonicalOpsTests { c.Logf("test %d: %v", i, test.about) ops := append([]bakery.Op(nil), test.ops...) c.Assert(bakery.CanonicalOps(ops), qt.DeepEquals, test.expect) // Verify that the original slice isn't changed. c.Assert(ops, qt.DeepEquals, test.ops) } } func TestMultipleOps(t *testing.T) { c := qt.New(t) oven := bakery.NewOven(bakery.OvenParams{}) ops := []bakery.Op{{"one", "read"}, {"one", "write"}, {"two", "read"}} m, err := oven.NewMacaroon(testContext, bakery.LatestVersion, nil, ops...) c.Assert(err, qt.IsNil) gotOps, conds, err := oven.VerifyMacaroon(testContext, macaroon.Slice{m.M()}) c.Assert(err, qt.IsNil) c.Assert(conds, qt.HasLen, 0) c.Assert(bakery.CanonicalOps(gotOps), qt.DeepEquals, ops) } func TestMultipleOpsInId(t *testing.T) { c := qt.New(t) oven := bakery.NewOven(bakery.OvenParams{}) ops := []bakery.Op{{"one", "read"}, {"one", "write"}, {"two", "read"}} m, err := oven.NewMacaroon(testContext, bakery.LatestVersion, nil, ops...) c.Assert(err, qt.IsNil) gotOps, conds, err := oven.VerifyMacaroon(testContext, macaroon.Slice{m.M()}) c.Assert(err, qt.IsNil) c.Assert(conds, qt.HasLen, 0) c.Assert(bakery.CanonicalOps(gotOps), qt.DeepEquals, ops) } func TestMultipleOpsInIdWithVersion1(t *testing.T) { c := qt.New(t) oven := bakery.NewOven(bakery.OvenParams{}) ops := []bakery.Op{{"one", "read"}, {"one", "write"}, {"two", "read"}} m, err := oven.NewMacaroon(testContext, bakery.Version1, nil, ops...) c.Assert(err, qt.IsNil) gotOps, conds, err := oven.VerifyMacaroon(testContext, macaroon.Slice{m.M()}) c.Assert(err, qt.IsNil) c.Assert(conds, qt.HasLen, 0) c.Assert(bakery.CanonicalOps(gotOps), qt.DeepEquals, ops) } macaroon-bakery-3.0.2/bakery/postgresrootkeystore/000077500000000000000000000000001464427415500224175ustar00rootroot00000000000000macaroon-bakery-3.0.2/bakery/postgresrootkeystore/export_test.go000066400000000000000000000003651464427415500253320ustar00rootroot00000000000000package postgresrootkeystore import "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/dbrootkeystore" var ( Clock = &clock NewBacking = &newBacking ) func Backing(keys *RootKeys) dbrootkeystore.Backing { return backing{keys} } macaroon-bakery-3.0.2/bakery/postgresrootkeystore/rootkey.go000066400000000000000000000067121464427415500244500ustar00rootroot00000000000000// Package postgreskeystore provides an implementation of bakery.RootKeyStore // that uses Postgres as a persistent store. package postgresrootkeystore import ( "database/sql" "sync" "time" "gopkg.in/errgo.v1" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/dbrootkeystore" ) // Variables defined so they can be overidden for testing. var ( clock dbrootkeystore.Clock newBacking = func(s *RootKeys) dbrootkeystore.Backing { return backing{s} } ) // TODO it would be nice if could make Policy // a type alias for dbrootkeystore.Policy, // but we want to be able to support versions // of Go from before type aliases were introduced. // Policy holds a store policy for root keys. type Policy dbrootkeystore.Policy // maxPolicyCache holds the maximum number of store policies that can // hold cached keys in a given RootKeys instance. // // 100 is probably overkill, given that practical systems will // likely only have a small number of active policies on any given // macaroon collection. const maxPolicyCache = 100 // RootKeys represents a cache of macaroon root keys. type RootKeys struct { keys *dbrootkeystore.RootKeys db *sql.DB table string stmts [numStmts]*sql.Stmt // initDBOnce guards initDBErr. initDBOnce sync.Once initDBErr error } // NewRootKeys returns a root-keys cache that // uses the given table in the given Postgres database for storage // and is limited in size to approximately the given size. // The table will be created lazily when the root key store // is first used. // // The returned RootKeys instance must be closed after use. // // It also creates other SQL resources using the table name // as a prefix. // // Use the NewStore method to obtain a RootKeyStore // implementation suitable for particular root key // lifetimes. func NewRootKeys(db *sql.DB, table string, maxCacheSize int) *RootKeys { return &RootKeys{ keys: dbrootkeystore.NewRootKeys(maxCacheSize, clock), db: db, table: table, } } // Close closes the RootKeys instance. This must be called after using the instance. func (s *RootKeys) Close() error { var retErr error for _, stmt := range s.stmts { if stmt == nil { continue } if err := stmt.Close(); err != nil && retErr == nil { retErr = err } } return errgo.Mask(retErr) } // NewStore returns a new RootKeyStore implementation that // stores and obtains root keys from the given collection. // // Root keys will be generated and stored following the // given store policy. // // It is expected that all collections passed to a given Store's // NewStore method should refer to the same underlying collection. func (s *RootKeys) NewStore(policy Policy) bakery.RootKeyStore { b := newBacking(s) return s.keys.NewStore(b, dbrootkeystore.Policy(policy)) } // backing implements dbrootkeystore.Backing by using Postgres as // a backing store. type backing struct { keys *RootKeys } // GetKey implements dbrootkeystore.Backing.GetKey. func (b backing) GetKey(id []byte) (dbrootkeystore.RootKey, error) { return b.keys.getKey(id) } // InsertKey implements dbrootkeystore.Backing.InsertKey. func (b backing) InsertKey(key dbrootkeystore.RootKey) error { return b.keys.insertKey(key) } // FindLatestKey implements dbrootkeystore.Backing.FindLatestKey. func (b backing) FindLatestKey(createdAfter, expiresAfter, expiresBefore time.Time) (dbrootkeystore.RootKey, error) { return b.keys.findLatestKey(createdAfter, expiresAfter, expiresBefore) } macaroon-bakery-3.0.2/bakery/postgresrootkeystore/rootkey_test.go000066400000000000000000000372311464427415500255070ustar00rootroot00000000000000package postgresrootkeystore_test import ( "context" "database/sql" "testing" "time" qt "github.com/frankban/quicktest" "github.com/frankban/quicktest/qtsuite" "github.com/juju/postgrestest" "gopkg.in/errgo.v1" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/dbrootkeystore" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/postgresrootkeystore" ) const testTable = "testrootkeys" type RootKeyStoreSuite struct { db_ *postgrestest.DB db *sql.DB store *postgresrootkeystore.RootKeys } func TestSuite(t *testing.T) { qtsuite.Run(qt.New(t), &RootKeyStoreSuite{}) } func (s *RootKeyStoreSuite) Init(c *qt.C) { db, err := postgrestest.New() if err == postgrestest.ErrDisabled { c.Skip("postgres testing is disabled") } c.Assert(err, qt.Equals, nil) store := postgresrootkeystore.NewRootKeys(db.DB, testTable, 1) c.Defer(func() { err := s.store.Close() c.Check(err, qt.Equals, nil) err = db.Close() c.Check(err, qt.Equals, nil) }) s.db = db.DB s.store = store } var epoch = time.Date(2200, time.January, 1, 0, 0, 0, 0, time.UTC) var IsValidWithPolicyTests = []struct { about string policy postgresrootkeystore.Policy now time.Time key dbrootkeystore.RootKey expect bool }{{ about: "success", policy: postgresrootkeystore.Policy{ GenerateInterval: 2 * time.Minute, ExpiryDuration: 3 * time.Minute, }, now: epoch.Add(20 * time.Minute), key: dbrootkeystore.RootKey{ Created: epoch.Add(19 * time.Minute), Expires: epoch.Add(24 * time.Minute), Id: []byte("id"), RootKey: []byte("key"), }, expect: true, }, { about: "empty root key", policy: postgresrootkeystore.Policy{ GenerateInterval: 2 * time.Minute, ExpiryDuration: 3 * time.Minute, }, now: epoch.Add(20 * time.Minute), key: dbrootkeystore.RootKey{}, expect: false, }, { about: "created too early", policy: postgresrootkeystore.Policy{ GenerateInterval: 2 * time.Minute, ExpiryDuration: 3 * time.Minute, }, now: epoch.Add(20 * time.Minute), key: dbrootkeystore.RootKey{ Created: epoch.Add(18*time.Minute - time.Millisecond), Expires: epoch.Add(24 * time.Minute), Id: []byte("id"), RootKey: []byte("key"), }, expect: false, }, { about: "expires too early", policy: postgresrootkeystore.Policy{ GenerateInterval: 2 * time.Minute, ExpiryDuration: 3 * time.Minute, }, now: epoch.Add(20 * time.Minute), key: dbrootkeystore.RootKey{ Created: epoch.Add(19 * time.Minute), Expires: epoch.Add(21 * time.Minute), Id: []byte("id"), RootKey: []byte("key"), }, expect: false, }, { about: "expires too late", policy: postgresrootkeystore.Policy{ GenerateInterval: 2 * time.Minute, ExpiryDuration: 3 * time.Minute, }, now: epoch.Add(20 * time.Minute), key: dbrootkeystore.RootKey{ Created: epoch.Add(19 * time.Minute), Expires: epoch.Add(25*time.Minute + time.Millisecond), Id: []byte("id"), RootKey: []byte("key"), }, expect: false, }} func (s *RootKeyStoreSuite) TestIsValidWithPolicy(c *qt.C) { for i, test := range IsValidWithPolicyTests { c.Logf("test %d: %v", i, test.about) c.Assert(test.key.IsValidWithPolicy(dbrootkeystore.Policy(test.policy), test.now), qt.Equals, test.expect) } } func (s *RootKeyStoreSuite) TestRootKeyUsesKeysValidWithPolicy(c *qt.C) { // We re-use the TestIsValidWithPolicy tests so that we // know that the mongo logic uses the same behaviour. var now time.Time c.Patch(postgresrootkeystore.Clock, clockVal(&now)) for i, test := range IsValidWithPolicyTests { c.Logf("test %d: %v", i, test.about) if test.key.RootKey == nil { // We don't store empty root keys in the database. c.Logf("skipping test with empty root key") continue } // Prime the table with the root key document. s.primeRootKeys(c, []dbrootkeystore.RootKey{test.key}) store := postgresrootkeystore.NewRootKeys(s.db, testTable, 10).NewStore(test.policy) now = test.now key, id, err := store.RootKey(context.Background()) c.Assert(err, qt.IsNil) if test.expect { c.Assert(string(id), qt.Equals, "id") c.Assert(string(key), qt.Equals, "key") } else { // If it didn't match then RootKey will have // generated a new key. c.Assert(key, qt.HasLen, 24) c.Assert(id, qt.HasLen, 32) } } } func (s *RootKeyStoreSuite) TestRootKey(c *qt.C) { now := epoch c.Patch(postgresrootkeystore.Clock, clockVal(&now)) store := postgresrootkeystore.NewRootKeys(s.db, testTable, 10).NewStore(postgresrootkeystore.Policy{ GenerateInterval: 2 * time.Minute, ExpiryDuration: 5 * time.Minute, }) key, id, err := store.RootKey(context.Background()) c.Assert(err, qt.IsNil) c.Assert(key, qt.HasLen, 24) c.Assert(id, qt.HasLen, 32) // If we get a key within the generate interval, we should // get the same one. now = epoch.Add(time.Minute) key1, id1, err := store.RootKey(context.Background()) c.Assert(err, qt.IsNil) c.Assert(key1, qt.DeepEquals, key) c.Assert(id1, qt.DeepEquals, id) // A different store instance should get the same root key. store1 := postgresrootkeystore.NewRootKeys(s.db, testTable, 10).NewStore(postgresrootkeystore.Policy{ GenerateInterval: 2 * time.Minute, ExpiryDuration: 5 * time.Minute, }) key1, id1, err = store1.RootKey(context.Background()) c.Assert(err, qt.IsNil) c.Assert(key1, qt.DeepEquals, key) c.Assert(id1, qt.DeepEquals, id) // After the generation interval has passed, we should generate a new key. now = epoch.Add(2*time.Minute + time.Second) key1, id1, err = store.RootKey(context.Background()) c.Assert(err, qt.IsNil) c.Assert(key, qt.HasLen, 24) c.Assert(id, qt.HasLen, 32) c.Assert(key1, qt.Not(qt.DeepEquals), key) c.Assert(id1, qt.Not(qt.DeepEquals), id) // The other store should pick it up too. key2, id2, err := store1.RootKey(context.Background()) c.Assert(err, qt.IsNil) c.Assert(key2, qt.DeepEquals, key1) c.Assert(id2, qt.DeepEquals, id1) } func (s *RootKeyStoreSuite) TestRootKeyDefaultGenerateInterval(c *qt.C) { now := epoch c.Patch(postgresrootkeystore.Clock, clockVal(&now)) store := postgresrootkeystore.NewRootKeys(s.db, testTable, 10).NewStore(postgresrootkeystore.Policy{ ExpiryDuration: 5 * time.Minute, }) key, id, err := store.RootKey(context.Background()) c.Assert(err, qt.IsNil) now = epoch.Add(5 * time.Minute) key1, id1, err := store.RootKey(context.Background()) c.Assert(err, qt.IsNil) c.Assert(key1, qt.DeepEquals, key) c.Assert(id1, qt.DeepEquals, id) now = epoch.Add(5*time.Minute + time.Millisecond) key1, id1, err = store.RootKey(context.Background()) c.Assert(err, qt.IsNil) c.Assert(string(key1), qt.Not(qt.Equals), string(key)) c.Assert(string(id1), qt.Not(qt.Equals), string(id)) } var preferredRootKeyTests = []struct { about string now time.Time keys []dbrootkeystore.RootKey policy postgresrootkeystore.Policy expectId []byte }{{ about: "latest creation time is preferred", now: epoch.Add(5 * time.Minute), keys: []dbrootkeystore.RootKey{{ Created: epoch.Add(4 * time.Minute), Expires: epoch.Add(15 * time.Minute), Id: []byte("id0"), RootKey: []byte("key0"), }, { Created: epoch.Add(5*time.Minute + 30*time.Second), Expires: epoch.Add(16 * time.Minute), Id: []byte("id1"), RootKey: []byte("key1"), }, { Created: epoch.Add(5 * time.Minute), Expires: epoch.Add(16 * time.Minute), Id: []byte("id2"), RootKey: []byte("key2"), }}, policy: postgresrootkeystore.Policy{ GenerateInterval: 5 * time.Minute, ExpiryDuration: 7 * time.Minute, }, expectId: []byte("id1"), }, { about: "ineligible keys are exluded", now: epoch.Add(5 * time.Minute), keys: []dbrootkeystore.RootKey{{ Created: epoch.Add(4 * time.Minute), Expires: epoch.Add(15 * time.Minute), Id: []byte("id0"), RootKey: []byte("key0"), }, { Created: epoch.Add(5 * time.Minute), Expires: epoch.Add(16*time.Minute + 30*time.Second), Id: []byte("id1"), RootKey: []byte("key1"), }, { Created: epoch.Add(6 * time.Minute), Expires: epoch.Add(time.Hour), Id: []byte("id2"), RootKey: []byte("key2"), }}, policy: postgresrootkeystore.Policy{ GenerateInterval: 5 * time.Minute, ExpiryDuration: 7 * time.Minute, }, expectId: []byte("id1"), }} func (s *RootKeyStoreSuite) TestPreferredRootKeyFromDatabase(c *qt.C) { var now time.Time c.Patch(postgresrootkeystore.Clock, clockVal(&now)) for i, test := range preferredRootKeyTests { c.Logf("%d: %v", i, test.about) s.primeRootKeys(c, test.keys) store := postgresrootkeystore.NewRootKeys(s.db, testTable, 10).NewStore(test.policy) now = test.now _, id, err := store.RootKey(context.Background()) c.Assert(err, qt.IsNil) c.Assert(id, qt.DeepEquals, test.expectId) } } func (s *RootKeyStoreSuite) TestPreferredRootKeyFromCache(c *qt.C) { var now time.Time c.Patch(postgresrootkeystore.Clock, clockVal(&now)) for i, test := range preferredRootKeyTests { c.Logf("%d: %v", i, test.about) s.primeRootKeys(c, test.keys) store := postgresrootkeystore.NewRootKeys(s.db, testTable, 10).NewStore(test.policy) // Ensure that all the keys are in cache by getting all of them. for _, key := range test.keys { got, err := store.Get(context.Background(), key.Id) c.Assert(err, qt.IsNil) c.Assert(got, qt.DeepEquals, key.RootKey) } // Remove all the keys from the collection so that // we know we must be acquiring them from the cache. s.primeRootKeys(c, nil) c.Logf("all keys removed") // Test that RootKey returns the expected key. now = test.now k, id, err := store.RootKey(context.Background()) c.Logf("rootKey %#v; id %#v; err %v", k, id, err) c.Assert(err, qt.IsNil) c.Assert(id, qt.DeepEquals, test.expectId) } } func (s *RootKeyStoreSuite) TestGet(c *qt.C) { now := epoch c.Patch(postgresrootkeystore.Clock, clockVal(&now)) var fetched []string c.Patch(postgresrootkeystore.NewBacking, func(keys *postgresrootkeystore.RootKeys) dbrootkeystore.Backing { b := postgresrootkeystore.Backing(keys) return &funcBacking{ Backing: b, getKey: func(id []byte) (dbrootkeystore.RootKey, error) { fetched = append(fetched, string(id)) return b.GetKey(id) }, } }) store := postgresrootkeystore.NewRootKeys(s.db, testTable, 5).NewStore(postgresrootkeystore.Policy{ GenerateInterval: 1 * time.Minute, ExpiryDuration: 30 * time.Minute, }) type idKey struct { id string key []byte } var keys []idKey keyIds := make(map[string]bool) for i := 0; i < 20; i++ { key, id, err := store.RootKey(context.Background()) c.Assert(err, qt.IsNil) c.Assert(keyIds[string(id)], qt.Equals, false) keys = append(keys, idKey{string(id), key}) now = now.Add(time.Minute + time.Second) } for i, k := range keys { key, err := store.Get(context.Background(), []byte(k.id)) c.Assert(err, qt.IsNil, qt.Commentf("key %d (%s)", i, k.id)) c.Assert(key, qt.DeepEquals, k.key, qt.Commentf("key %d (%s)", i, k.id)) } // Check that the keys are cached. // // Since the cache size is 5, the most recent 5 items will be in // the primary cache; the 5 items before that will be in the old // cache and nothing else will be cached. // // The first time we fetch an item from the old cache, a new // primary cache will be allocated, all existing items in the // old cache except that item will be evicted, and all items in // the current primary cache moved to the old cache. // // The upshot of that is that all but the first 6 calls to Get // should result in a database fetch. c.Logf("testing cache") fetched = nil for i := len(keys) - 1; i >= 0; i-- { k := keys[i] key, err := store.Get(context.Background(), []byte(k.id)) c.Assert(err, qt.IsNil) c.Assert(err, qt.IsNil, qt.Commentf("key %d (%s)", i, k.id)) c.Assert(key, qt.DeepEquals, k.key, qt.Commentf("key %d (%s)", i, k.id)) } c.Assert(len(fetched), qt.Equals, len(keys)-6) for i, id := range fetched { c.Assert(id, qt.Equals, keys[len(keys)-6-i-1].id) } } func (s *RootKeyStoreSuite) TestGetCachesMisses(c *qt.C) { var fetched []string c.Patch(postgresrootkeystore.NewBacking, func(keys *postgresrootkeystore.RootKeys) dbrootkeystore.Backing { b := postgresrootkeystore.Backing(keys) return &funcBacking{ Backing: b, getKey: func(id []byte) (dbrootkeystore.RootKey, error) { fetched = append(fetched, string(id)) return b.GetKey(id) }, } }) store := postgresrootkeystore.NewRootKeys(s.db, testTable, 5).NewStore(postgresrootkeystore.Policy{ GenerateInterval: 1 * time.Minute, ExpiryDuration: 30 * time.Minute, }) key, err := store.Get(context.Background(), []byte("foo")) c.Assert(errgo.Cause(err), qt.Equals, bakery.ErrNotFound) c.Assert(key, qt.IsNil) c.Assert(fetched, qt.DeepEquals, []string{"foo"}) fetched = nil key, err = store.Get(context.Background(), []byte("foo")) c.Assert(err, qt.Equals, bakery.ErrNotFound) c.Assert(key, qt.IsNil) c.Assert(fetched, qt.IsNil) } func (s *RootKeyStoreSuite) TestGetExpiredItemFromCache(c *qt.C) { now := epoch c.Patch(postgresrootkeystore.Clock, clockVal(&now)) store := postgresrootkeystore.NewRootKeys(s.db, testTable, 10).NewStore(postgresrootkeystore.Policy{ ExpiryDuration: 5 * time.Minute, }) _, id, err := store.RootKey(context.Background()) c.Assert(err, qt.IsNil) c.Patch(postgresrootkeystore.NewBacking, func(keys *postgresrootkeystore.RootKeys) dbrootkeystore.Backing { return &funcBacking{ Backing: postgresrootkeystore.Backing(keys), getKey: func(id []byte) (dbrootkeystore.RootKey, error) { c.Errorf("FindId unexpectedly called") return dbrootkeystore.RootKey{}, nil }, } }) now = epoch.Add(15 * time.Minute) _, err = store.Get(context.Background(), id) c.Assert(err, qt.Equals, bakery.ErrNotFound) } const sqlTimeFormat = "2006-01-02 15:04:05.9999-07" func (s *RootKeyStoreSuite) TestKeyExpiration(c *qt.C) { keys := postgresrootkeystore.NewRootKeys(s.db, testTable, 5) _, id1, err := keys.NewStore(postgresrootkeystore.Policy{ ExpiryDuration: 100 * time.Millisecond, GenerateInterval: time.Nanosecond, }).RootKey(context.Background()) c.Assert(err, qt.IsNil) _, id2, err := keys.NewStore(postgresrootkeystore.Policy{ ExpiryDuration: time.Hour, }).RootKey(context.Background()) c.Assert(err, qt.IsNil) c.Assert(string(id2), qt.Not(qt.Equals), string(id1)) // Sanity check that the keys are in the collection. var n int err = s.db.QueryRow(`SELECT count(id) FROM ` + testTable).Scan(&n) c.Assert(err, qt.Equals, nil) c.Assert(n, qt.Equals, 2) // Sleep past the expiry time of the first key. time.Sleep(150 * time.Millisecond) // Use a store with a short generate interval to force // another key to be generated, which should trigger // the expiration check (the trigger is on INSERT). _, _, err = keys.NewStore(postgresrootkeystore.Policy{ GenerateInterval: time.Nanosecond, ExpiryDuration: time.Hour, }).RootKey(context.Background()) c.Assert(err, qt.Equals, nil) _, err = postgresrootkeystore.Backing(s.store).GetKey(id1) c.Assert(errgo.Cause(err), qt.Equals, bakery.ErrNotFound) } // primeRootKeys deletes all rows from the root key table // and inserts the given keys. func (s *RootKeyStoreSuite) primeRootKeys(c *qt.C, keys []dbrootkeystore.RootKey) { // Ignore any error from the delete - it's probably happening // because the table does not exist yet. s.db.Exec(`DELETE FROM ` + testTable) for _, key := range keys { err := postgresrootkeystore.Backing(s.store).InsertKey(key) c.Assert(err, qt.IsNil) } } func clockVal(t *time.Time) dbrootkeystore.Clock { return clockFunc(func() time.Time { return *t }) } type clockFunc func() time.Time func (f clockFunc) Now() time.Time { return f() } type funcBacking struct { dbrootkeystore.Backing getKey func(id []byte) (dbrootkeystore.RootKey, error) } func (b *funcBacking) GetKey(id []byte) (dbrootkeystore.RootKey, error) { if b.getKey == nil { return b.Backing.GetKey(id) } return b.getKey(id) } macaroon-bakery-3.0.2/bakery/postgresrootkeystore/sql.go000066400000000000000000000116511464427415500235510ustar00rootroot00000000000000package postgresrootkeystore import ( "bytes" "database/sql" "fmt" "text/template" "time" "gopkg.in/errgo.v1" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/dbrootkeystore" ) type stmtId int const ( findIdStmt stmtId = iota findBestRootKeyStmt insertKeyStmt numStmts ) var initStatements = ` BEGIN; -- Set up an advisory lock so that only one thread can issue the statements -- below at a time, to avoid issues cause by concurrent updates (especially in -- the 'CREATE OR REPLACE FUNCTION' statement). -- The lock value is random, and it should be shared by all callers of this -- script. It is automatically released on commit. SELECT pg_advisory_xact_lock(34577509137); CREATE TABLE IF NOT EXISTS {{.Table}} ( id BYTEA PRIMARY KEY NOT NULL, rootkey BYTEA, created TIMESTAMP WITH TIME ZONE NOT NULL, expires TIMESTAMP WITH TIME ZONE NOT NULL ); CREATE OR REPLACE FUNCTION {{.ExpireFunc}}() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN DELETE FROM {{.Table}} WHERE expires < NOW(); RETURN NEW; END; $$; CREATE INDEX IF NOT EXISTS {{.CreateIndex}} ON {{.Table}} (created); CREATE INDEX IF NOT EXISTS {{.ExpireIndex}} ON {{.Table}} (expires); DROP TRIGGER IF EXISTS {{.ExpireTrigger}} ON {{.Table}}; CREATE TRIGGER {{.ExpireTrigger}} BEFORE INSERT ON {{.Table}} EXECUTE PROCEDURE {{.ExpireFunc}}(); COMMIT; ` type templateParams struct { Table string ExpireFunc string CreateIndex string ExpireIndex string ExpireTrigger string } func (s *RootKeys) initDB() error { s.initDBOnce.Do(func() { s.initDBErr = s._initDB() }) if s.initDBErr != nil { return errgo.Notef(s.initDBErr, "cannot initialize database") } return nil } func (s *RootKeys) _initDB() error { p := &templateParams{ Table: s.table, ExpireFunc: s.table + "_expire_func", CreateIndex: s.table + "_index_create", ExpireIndex: s.table + "_index_expire", ExpireTrigger: s.table + "_trigger", } if _, err := s.db.Exec(templateVal(p, initStatements)); err != nil { return errgo.Notef(err, "cannot initialize table") } if err := s.prepareAll(p); err != nil { return errgo.Notef(err, "cannot prepare statements") } return nil } func (s *RootKeys) prepareAll(p *templateParams) error { if err := s.prepareFindId(p); err != nil { return errgo.Mask(err) } if err := s.prepareFindBestRootKey(p); err != nil { return errgo.Mask(err) } if err := s.prepareInsertKey(p); err != nil { return errgo.Mask(err) } return nil } func (s *RootKeys) prepareFindId(p *templateParams) error { return s.prepare(findIdStmt, p, ` SELECT id, created, expires, rootkey FROM {{.Table}} WHERE id=$1 `) } func (s *RootKeys) getKey(id []byte) (dbrootkeystore.RootKey, error) { if err := s.initDB(); err != nil { return dbrootkeystore.RootKey{}, errgo.Mask(err) } var key dbrootkeystore.RootKey err := s.stmts[findIdStmt].QueryRow(id).Scan( &key.Id, &key.Created, &key.Expires, &key.RootKey, ) switch { case err == sql.ErrNoRows: return dbrootkeystore.RootKey{}, bakery.ErrNotFound case err != nil: return dbrootkeystore.RootKey{}, errgo.Mask(err) } return key, nil } func (s *RootKeys) prepareFindBestRootKey(p *templateParams) error { return s.prepare(findBestRootKeyStmt, p, ` SELECT id, created, expires, rootkey FROM {{.Table}} WHERE created >= $1 AND expires >= $2 AND expires <= $3 ORDER BY created DESC `) } func (s *RootKeys) findLatestKey(createdAfter, expiresAfter, expiresBefore time.Time) (dbrootkeystore.RootKey, error) { if err := s.initDB(); err != nil { return dbrootkeystore.RootKey{}, errgo.Mask(err) } var key dbrootkeystore.RootKey err := s.stmts[findBestRootKeyStmt].QueryRow( createdAfter, expiresAfter, expiresBefore, ).Scan( &key.Id, &key.Created, &key.Expires, &key.RootKey, ) if err == sql.ErrNoRows || err == nil { return key, nil } return dbrootkeystore.RootKey{}, errgo.Mask(err) } func (s *RootKeys) prepareInsertKey(p *templateParams) error { return s.prepare(insertKeyStmt, p, ` INSERT into {{.Table}} (id, rootkey, created, expires) VALUES ($1, $2, $3, $4) `) } func (s *RootKeys) insertKey(key dbrootkeystore.RootKey) error { if err := s.initDB(); err != nil { return errgo.Mask(err) } _, err := s.stmts[insertKeyStmt].Exec(key.Id, key.RootKey, key.Created, key.Expires) return errgo.Mask(err) } func (s *RootKeys) prepare(id stmtId, p *templateParams, tmpl string) error { if s.stmts[id] != nil { panic(fmt.Sprintf("statement %v prepared twice", id)) } stmt, err := s.db.Prepare(templateVal(p, tmpl)) if err != nil { return errgo.Notef(err, "statement %v (%q) invalid", id, templateVal(p, tmpl)) } s.stmts[id] = stmt return nil } func templateVal(p *templateParams, s string) string { tmpl := template.Must(template.New("").Parse(s)) var buf bytes.Buffer if err := tmpl.Execute(&buf, p); err != nil { panic(errgo.Notef(err, "cannot create initialization statements")) } return buf.String() } macaroon-bakery-3.0.2/bakery/slice.go000066400000000000000000000075671464427415500175240ustar00rootroot00000000000000package bakery import ( "context" "fmt" "time" "gopkg.in/errgo.v1" "gopkg.in/macaroon.v2" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/checkers" ) // Slice holds a slice of unbound macaroons. type Slice []*Macaroon // Bind prepares the macaroon slice for use in a request. This must be // done before presenting the macaroons to a service for use as // authorization tokens. The result will only be valid // if s contains discharge macaroons for all third party // caveats. // // All the macaroons in the returned slice will be copies // of this in s, not references. func (s Slice) Bind() macaroon.Slice { if len(s) == 0 { return nil } ms := make(macaroon.Slice, len(s)) ms[0] = s[0].M().Clone() rootSig := ms[0].Signature() for i, m := range s[1:] { m1 := m.M().Clone() m1.Bind(rootSig) ms[i+1] = m1 } return ms } // Purge returns a new slice holding all macaroons in s // that expire after the given time. func (ms Slice) Purge(t time.Time) Slice { ms1 := make(Slice, 0, len(ms)) for i, m := range ms { et, ok := checkers.ExpiryTime(m.Namespace(), m.M().Caveats()) if !ok || et.After(t) { ms1 = append(ms1, m) } else if i == 0 { // The primary macaroon has expired, so all its discharges // have expired too. // TODO purge all discharge macaroons when the macaroon // containing their third-party caveat expires. return nil } } return ms1 } // DischargeAll discharges all the third party caveats in the slice for // which discharge macaroons are not already present, using getDischarge // to acquire the discharge macaroons. It always returns the slice with // any acquired discharge macaroons added, even on error. It returns an // error if all the discharges could not be acquired. // // Note that this differs from DischargeAll in that it can be given several existing // discharges, and that the resulting discharges are not bound to the primary, // so it's still possible to add caveats and reacquire expired discharges // without reacquiring the primary macaroon. func (ms Slice) DischargeAll(ctx context.Context, getDischarge func(ctx context.Context, cav macaroon.Caveat, encryptedCaveat []byte) (*Macaroon, error), localKey *KeyPair) (Slice, error) { if len(ms) == 0 { return nil, errgo.Newf("no macaroons to discharge") } ms1 := make(Slice, len(ms)) copy(ms1, ms) // have holds the keys of all the macaroon ids in the slice. type needCaveat struct { // cav holds the caveat that needs discharge. cav macaroon.Caveat // encryptedCaveat holds encrypted caveat // if it was held externally. encryptedCaveat []byte } var need []needCaveat have := make(map[string]bool) for _, m := range ms[1:] { have[string(m.M().Id())] = true } // addCaveats adds any required third party caveats to the need slice // that aren't already present . addCaveats := func(m *Macaroon) { for _, cav := range m.M().Caveats() { if len(cav.VerificationId) == 0 || have[string(cav.Id)] { continue } need = append(need, needCaveat{ cav: cav, encryptedCaveat: m.caveatData[string(cav.Id)], }) } } for _, m := range ms { addCaveats(m) } var errs []error for len(need) > 0 { cav := need[0] need = need[1:] var dm *Macaroon var err error if localKey != nil && cav.cav.Location == "local" { // TODO use a small caveat id. dm, err = Discharge(ctx, DischargeParams{ Key: localKey, Checker: localDischargeChecker, Caveat: cav.encryptedCaveat, Id: cav.cav.Id, Locator: emptyLocator{}, }) } else { dm, err = getDischarge(ctx, cav.cav, cav.encryptedCaveat) } if err != nil { errs = append(errs, errgo.NoteMask(err, fmt.Sprintf("cannot get discharge from %q", cav.cav.Location), errgo.Any)) continue } ms1 = append(ms1, dm) addCaveats(dm) } if errs != nil { // TODO log other errors? Return them all? return ms1, errgo.Mask(errs[0], errgo.Any) } return ms1, nil } macaroon-bakery-3.0.2/bakery/slice_test.go000066400000000000000000000145551464427415500205560ustar00rootroot00000000000000package bakery_test import ( "context" "fmt" "testing" "time" qt "github.com/frankban/quicktest" "gopkg.in/errgo.v1" "gopkg.in/macaroon.v2" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/checkers" ) func TestAddMoreCaveats(t *testing.T) { c := qt.New(t) getDischarge := func(_ context.Context, cav macaroon.Caveat, payload []byte) (*bakery.Macaroon, error) { c.Check(payload, qt.IsNil) m, err := bakery.NewMacaroon([]byte("root key "+string(cav.Id)), cav.Id, "", bakery.LatestVersion, nil) c.Assert(err, qt.Equals, nil) return m, nil } rootKey := []byte("root key") m, err := bakery.NewMacaroon(rootKey, []byte("id0"), "loc0", bakery.LatestVersion, testChecker.Namespace()) c.Assert(err, qt.Equals, nil) err = m.M().AddThirdPartyCaveat([]byte("root key id1"), []byte("id1"), "somewhere") c.Assert(err, qt.Equals, nil) ms, err := bakery.Slice{m}.DischargeAll(testContext, getDischarge, nil) c.Assert(err, qt.Equals, nil) c.Assert(ms, qt.HasLen, 2) mms := ms.Bind() c.Assert(mms, qt.HasLen, len(ms)) err = mms[0].Verify(rootKey, alwaysOK, mms[1:]) c.Assert(err, qt.Equals, nil) // Add another caveat and to the root macaroon and discharge it. err = ms[0].M().AddThirdPartyCaveat([]byte("root key id2"), []byte("id2"), "somewhere else") c.Assert(err, qt.Equals, nil) ms, err = ms.DischargeAll(testContext, getDischarge, nil) c.Assert(err, qt.Equals, nil) c.Assert(ms, qt.HasLen, 3) mms = ms.Bind() err = mms[0].Verify(rootKey, alwaysOK, mms[1:]) c.Assert(err, qt.Equals, nil) // Check that we can remove the original discharge and still re-acquire it OK. ms = bakery.Slice{ms[0], ms[2]} ms, err = ms.DischargeAll(testContext, getDischarge, nil) c.Assert(err, qt.Equals, nil) c.Assert(ms, qt.HasLen, 3) mms = ms.Bind() err = mms[0].Verify(rootKey, alwaysOK, mms[1:]) c.Assert(err, qt.Equals, nil) } func TestPurge(t *testing.T) { c := qt.New(t) t0 := time.Date(2000, time.October, 1, 12, 0, 0, 0, time.UTC) clock := &stoppedClock{ t: t0, } ctx := checkers.ContextWithClock(testContext, clock) checkCond := func(cond string) error { return testChecker.CheckFirstPartyCaveat(ctx, cond) } rootKey := []byte("root key") m, err := bakery.NewMacaroon(rootKey, []byte("id0"), "loc0", bakery.LatestVersion, testChecker.Namespace()) c.Assert(err, qt.Equals, nil) err = m.AddCaveat(ctx, checkers.TimeBeforeCaveat(t0.Add(time.Hour)), nil, nil) c.Assert(err, qt.Equals, nil) err = m.M().AddThirdPartyCaveat([]byte("root key id1"), []byte("id1"), "somewhere") c.Assert(err, qt.Equals, nil) ms := bakery.Slice{m} getDischarge := func(_ context.Context, cav macaroon.Caveat, payload []byte) (*bakery.Macaroon, error) { c.Check(payload, qt.IsNil) m, err := bakery.NewMacaroon([]byte("root key "+string(cav.Id)), cav.Id, "", bakery.LatestVersion, testChecker.Namespace()) c.Assert(err, qt.Equals, nil) err = m.AddCaveat(ctx, checkers.TimeBeforeCaveat(clock.t.Add(time.Minute)), nil, nil) c.Assert(err, qt.Equals, nil) return m, nil } ms, err = ms.DischargeAll(testContext, getDischarge, nil) c.Assert(err, qt.Equals, nil) c.Assert(ms, qt.HasLen, 2) mms := ms.Bind() err = mms[0].Verify(rootKey, checkCond, mms[1:]) c.Assert(err, qt.Equals, nil) // Sanity check that verification fails when the discharge time has expired. clock.t = t0.Add(2 * time.Minute) err = mms[0].Verify(rootKey, checkCond, mms[1:]) c.Assert(err, qt.ErrorMatches, `.*: macaroon has expired`) // Purge removes the discharge macaroon when it's out of date. ms = ms.Purge(clock.t) c.Assert(ms, qt.HasLen, 1) // Reacquire a discharge macaroon. ms, err = ms.DischargeAll(testContext, getDischarge, nil) c.Assert(err, qt.Equals, nil) c.Assert(ms, qt.HasLen, 2) // The macaroons should now be valid again. mms = ms.Bind() err = mms[0].Verify(rootKey, checkCond, mms[1:]) c.Assert(err, qt.Equals, nil) // Check that when the time has gone beyond the primary // macaroon's expiry time, Purge removes all the macaroons. // Reacquire a discharge macaroon just before the primary // macaroon's expiry time. clock.t = t0.Add(time.Hour - time.Second) ms = ms.Purge(clock.t) c.Assert(ms, qt.HasLen, 1) ms, err = ms.DischargeAll(testContext, getDischarge, nil) c.Assert(err, qt.Equals, nil) c.Assert(ms, qt.HasLen, 2) // The macaroons should now be valid again. mms = ms.Bind() err = mms[0].Verify(rootKey, checkCond, mms[1:]) c.Assert(err, qt.Equals, nil) // But once we've passed the hour, the primary expires // even though the discharge is valid, and purging // removes both primary and discharge. ms = ms.Purge(t0.Add(time.Hour + time.Millisecond)) c.Assert(ms, qt.HasLen, 0) } func TestDischargeAllAcquiresManyMacaroonsAsPossible(t *testing.T) { c := qt.New(t) failIds := map[string]bool{ "id1": true, "id3": true, } getDischarge := func(_ context.Context, cav macaroon.Caveat, payload []byte) (*bakery.Macaroon, error) { if failIds[string(cav.Id)] { return nil, errgo.Newf("discharge failure on %q", cav.Id) } m, err := bakery.NewMacaroon([]byte("root key "+string(cav.Id)), cav.Id, "", bakery.LatestVersion, nil) c.Assert(err, qt.Equals, nil) return m, nil } rootKey := []byte("root key") m, err := bakery.NewMacaroon(rootKey, []byte("id-root"), "", bakery.LatestVersion, testChecker.Namespace()) c.Assert(err, qt.Equals, nil) for i := 0; i < 5; i++ { id := fmt.Sprintf("id%d", i) err = m.M().AddThirdPartyCaveat([]byte("root key "+id), []byte(id), "somewhere") c.Assert(err, qt.Equals, nil) } ms := bakery.Slice{m} ms, err = ms.DischargeAll(testContext, getDischarge, nil) c.Check(err, qt.ErrorMatches, `cannot get discharge from "somewhere": discharge failure on "id1"`) c.Assert(ms, qt.HasLen, 4) // Try again without id1 failing - we should acquire one more discharge. // Mark the other ones as failing because we shouldn't be trying to acquire // them because they're already in the slice. failIds = map[string]bool{ "id0": true, "id3": true, "id4": true, } ms, err = ms.DischargeAll(testContext, getDischarge, nil) c.Check(err, qt.ErrorMatches, `cannot get discharge from "somewhere": discharge failure on "id3"`) c.Assert(ms, qt.HasLen, 5) failIds["id3"] = false ms, err = ms.DischargeAll(testContext, getDischarge, nil) c.Check(err, qt.Equals, nil) c.Assert(ms, qt.HasLen, 6) mms := ms.Bind() err = mms[0].Verify(rootKey, alwaysOK, mms[1:]) c.Assert(err, qt.Equals, nil) } macaroon-bakery-3.0.2/bakery/store.go000066400000000000000000000032111464427415500175370ustar00rootroot00000000000000package bakery import ( "context" "sync" ) // RootKeyStore defines store for macaroon root keys. type RootKeyStore interface { // Get returns the root key for the given id. // If the item is not there, it returns ErrNotFound. Get(ctx context.Context, id []byte) ([]byte, error) // RootKey returns the root key to be used for making a new // macaroon, and an id that can be used to look it up later with // the Get method. // // Note that the root keys should remain available for as long // as the macaroons using them are valid. // // Note that there is no need for it to return a new root key // for every call - keys may be reused, although some key // cycling is over time is advisable. RootKey(ctx context.Context) (rootKey []byte, id []byte, err error) } // NewMemRootKeyStore returns an implementation of // Store that generates a single key and always // returns that from RootKey. The same id ("0") is always // used. func NewMemRootKeyStore() RootKeyStore { return new(memRootKeyStore) } type memRootKeyStore struct { mu sync.Mutex key []byte } // Get implements Store.Get. func (s *memRootKeyStore) Get(_ context.Context, id []byte) ([]byte, error) { s.mu.Lock() defer s.mu.Unlock() if len(id) != 1 || id[0] != '0' || s.key == nil { return nil, ErrNotFound } return s.key, nil } // RootKey implements Store.RootKey by always returning the same root // key. func (s *memRootKeyStore) RootKey(context.Context) (rootKey, id []byte, err error) { s.mu.Lock() defer s.mu.Unlock() if s.key == nil { newKey, err := randomBytes(24) if err != nil { return nil, nil, err } s.key = newKey } return s.key, []byte("0"), nil } macaroon-bakery-3.0.2/bakery/store_test.go000066400000000000000000000016031464427415500206010ustar00rootroot00000000000000package bakery_test import ( "testing" qt "github.com/frankban/quicktest" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" ) func TestMemStore(t *testing.T) { c := qt.New(t) store := bakery.NewMemRootKeyStore() key, err := store.Get(nil, []byte("x")) c.Assert(err, qt.Equals, bakery.ErrNotFound) c.Assert(key, qt.IsNil) key, err = store.Get(nil, []byte("0")) c.Assert(err, qt.Equals, bakery.ErrNotFound) c.Assert(key, qt.IsNil) key, id, err := store.RootKey(nil) c.Assert(err, qt.IsNil) c.Assert(key, qt.HasLen, 24) c.Assert(string(id), qt.Equals, "0") key1, id1, err := store.RootKey(nil) c.Assert(err, qt.IsNil) c.Assert(key1, qt.DeepEquals, key) c.Assert(id1, qt.DeepEquals, id) key2, err := store.Get(nil, id) c.Assert(err, qt.IsNil) c.Assert(key2, qt.DeepEquals, key) _, err = store.Get(nil, []byte("1")) c.Assert(err, qt.Equals, bakery.ErrNotFound) } macaroon-bakery-3.0.2/bakery/version.go000066400000000000000000000014341464427415500200750ustar00rootroot00000000000000package bakery import "gopkg.in/macaroon.v2" // Version represents a version of the bakery protocol. type Version int const ( // In version 0, discharge-required errors use status 407 Version0 Version = 0 // In version 1, discharge-required errors use status 401. Version1 Version = 1 // In version 2, binary macaroons and caveat ids are supported. Version2 Version = 2 // In version 3, we support operations associated with macaroons // and external third party caveats. Version3 Version = 3 LatestVersion = Version3 ) // MacaroonVersion returns the macaroon version that should // be used with the given bakery Version. func MacaroonVersion(v Version) macaroon.Version { switch v { case Version0, Version1: return macaroon.V1 default: return macaroon.V2 } } macaroon-bakery-3.0.2/bakerytest/000077500000000000000000000000001464427415500167575ustar00rootroot00000000000000macaroon-bakery-3.0.2/bakerytest/bakerytest.go000066400000000000000000000153601464427415500214700ustar00rootroot00000000000000// Package bakerytest provides test helper functions for // the bakery. package bakerytest import ( "context" "crypto/tls" "net/http" "net/http/httptest" "sync" "github.com/julienschmidt/httprouter" "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/bakery/checkers" "github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery" ) // Discharger represents a third party caveat discharger server. type Discharger struct { server *httptest.Server // Mux holds the HTTP multiplexor used by // the discharger server. Mux *httprouter.Router // Key holds the discharger's private key. Key *bakery.KeyPair // Locator holds the third party locator // used when adding a third party caveat // returned by a third party caveat checker. Locator bakery.ThirdPartyLocator // CheckerP is called to check third party caveats when they're // discharged. It defaults to NopThirdPartyCaveatCheckerP. CheckerP httpbakery.ThirdPartyCaveatCheckerP // Checker is the deprecated version of CheckerP, and will be // ignored if CheckerP is non-nil. Checker httpbakery.ThirdPartyCaveatChecker } // NewDischarger returns a new discharger server that can be used to // discharge third party caveats. It uses the given locator to add third // party caveats returned by the Checker. The discharger also acts as a // locator, returning locator information for itself only. // // The returned discharger should be closed after use. // // This should not be used concurrently unless httpbakery.AllowInsecureThirdPartyLocator // is set, because otherwise it needs to run a TLS server and modify http.DefaultTransport // to allow insecure connections. func NewDischarger(locator bakery.ThirdPartyLocator) *Discharger { key, err := bakery.GenerateKey() if err != nil { panic(err) } d := &Discharger{ Mux: httprouter.New(), Key: key, Locator: locator, } handler := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { d.Mux.ServeHTTP(w, req) }) if httpbakery.AllowInsecureThirdPartyLocator { d.server = httptest.NewServer(handler) } else { d.server = httptest.NewTLSServer(handler) startSkipVerify() } bd := httpbakery.NewDischarger(httpbakery.DischargerParams{ Key: key, Locator: locator, CheckerP: d, }) d.AddHTTPHandlers(bd.Handlers()) return d } // AddHTTPHandlers adds the given HTTP handlers to the // set of endpoints handled by the discharger. func (d *Discharger) AddHTTPHandlers(hs []httprequest.Handler) { for _, h := range hs { d.Mux.Handle(h.Method, h.Path, h.Handle) } } // Close shuts down the server. It may be called more than // once on the same discharger. func (d *Discharger) Close() { if d.server == nil { return } d.server.Close() stopSkipVerify() d.server = nil } // Location returns the location of the discharger, suitable // for setting as the location in a third party caveat. // This will be the URL of the server. func (d *Discharger) Location() string { return d.server.URL } // PublicKeyForLocation implements bakery.PublicKeyLocator // by returning information on the discharger's server location // only. func (d *Discharger) ThirdPartyInfo(ctx context.Context, loc string) (bakery.ThirdPartyInfo, error) { if loc == d.Location() { return bakery.ThirdPartyInfo{ PublicKey: d.Key.Public, Version: bakery.LatestVersion, }, nil } return bakery.ThirdPartyInfo{}, bakery.ErrNotFound } // DischargeMacaroon returns a discharge macaroon // for the given caveat information with the given // caveats added. It assumed the actual third party // caveat has already been checked. func (d *Discharger) DischargeMacaroon( ctx context.Context, cav *bakery.ThirdPartyCaveatInfo, caveats []checkers.Caveat, ) (*bakery.Macaroon, error) { return bakery.Discharge(ctx, bakery.DischargeParams{ Id: cav.Id, Caveat: cav.Caveat, Key: d.Key, Checker: bakery.ThirdPartyCaveatCheckerFunc(func(ctx context.Context, cav *bakery.ThirdPartyCaveatInfo) ([]checkers.Caveat, error) { return caveats, nil }), Locator: d.Locator, }) } var ErrTokenNotRecognized = errgo.New("discharge token not recognized") // CheckThirdPartyCaveat implements httpbakery.ThirdPartyCaveatCheckerP // by calling d.CheckerP, or d.Checker if that's nil. func (d *Discharger) CheckThirdPartyCaveat(ctx context.Context, p httpbakery.ThirdPartyCaveatCheckerParams) ([]checkers.Caveat, error) { if d.CheckerP != nil { return d.CheckerP.CheckThirdPartyCaveat(ctx, p) } if d.Checker == nil { return nil, nil } return d.Checker.CheckThirdPartyCaveat(ctx, p.Caveat, p.Request, p.Token) } // ConditionParser adapts the given function into an httpbakery.ThirdPartyCaveatCheckerP. // It parses the caveat's condition and calls the function with the result. func ConditionParser(check func(cond, arg string) ([]checkers.Caveat, error)) httpbakery.ThirdPartyCaveatCheckerP { f := func(ctx context.Context, p httpbakery.ThirdPartyCaveatCheckerParams) ([]checkers.Caveat, error) { cond, arg, err := checkers.ParseCaveat(string(p.Caveat.Condition)) if err != nil { return nil, err } return check(cond, arg) } return httpbakery.ThirdPartyCaveatCheckerPFunc(f) } // ConditionParserP adapts the given function into an httpbakery.ThirdPartyCaveatChecker. // It parses the caveat's condition and calls the function with the result. func ConditionParserP(check func(cond, arg string) ([]checkers.Caveat, error)) httpbakery.ThirdPartyCaveatChecker { f := func(ctx context.Context, req *http.Request, cav *bakery.ThirdPartyCaveatInfo, token *httpbakery.DischargeToken) ([]checkers.Caveat, error) { cond, arg, err := checkers.ParseCaveat(string(cav.Condition)) if err != nil { return nil, err } return check(cond, arg) } return httpbakery.ThirdPartyCaveatCheckerFunc(f) } var skipVerify struct { mu sync.Mutex refCount int oldSkipVerify bool } func startSkipVerify() { v := &skipVerify v.mu.Lock() defer v.mu.Unlock() if v.refCount++; v.refCount > 1 { return } transport, ok := http.DefaultTransport.(*http.Transport) if !ok { return } if transport.TLSClientConfig != nil { v.oldSkipVerify = transport.TLSClientConfig.InsecureSkipVerify transport.TLSClientConfig.InsecureSkipVerify = true } else { v.oldSkipVerify = false transport.TLSClientConfig = &tls.Config{ InsecureSkipVerify: true, } } } func stopSkipVerify() { v := &skipVerify v.mu.Lock() defer v.mu.Unlock() if v.refCount--; v.refCount > 0 { return } transport, ok := http.DefaultTransport.(*http.Transport) if !ok { return } // technically this doesn't return us to the original state, // as TLSClientConfig may have been nil before but won't // be now, but that should be equivalent. transport.TLSClientConfig.InsecureSkipVerify = v.oldSkipVerify } macaroon-bakery-3.0.2/bakerytest/bakerytest_test.go000066400000000000000000000320621464427415500225250ustar00rootroot00000000000000package bakerytest_test import ( "context" "fmt" "net/http" "net/url" "sync" "testing" "time" qt "github.com/frankban/quicktest" "github.com/julienschmidt/httprouter" "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/bakery/checkers" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakerytest" "github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery" ) var dischargeOp = bakery.Op{"thirdparty", "x"} func TestDischargerSimple(t *testing.T) { c := qt.New(t) d := bakerytest.NewDischarger(nil) defer d.Close() client := httpbakery.NewClient() b := bakery.New(bakery.BakeryParams{ Location: "here", Locator: d, Key: bakery.MustGenerateKey(), }) m, err := b.Oven.NewMacaroon(context.Background(), bakery.LatestVersion, []checkers.Caveat{{ Location: d.Location(), Condition: "something", }}, dischargeOp) c.Assert(err, qt.IsNil) ms, err := client.DischargeAll(context.Background(), m) c.Assert(err, qt.IsNil) c.Assert(ms, qt.HasLen, 2) _, err = b.Checker.Auth(ms).Allow(context.Background(), dischargeOp) c.Assert(err, qt.IsNil) } func TestDischargerTwoLevels(t *testing.T) { c := qt.New(t) client := httpbakery.NewClient() d1checker := func(cond, arg string) ([]checkers.Caveat, error) { if cond != "xtrue" { return nil, fmt.Errorf("caveat refused") } return nil, nil } d1 := bakerytest.NewDischarger(nil) d1.CheckerP = bakerytest.ConditionParser(d1checker) defer d1.Close() d2checker := func(cond, arg string) ([]checkers.Caveat, error) { return []checkers.Caveat{{ Location: d1.Location(), Condition: "x" + cond, }}, nil } d2 := bakerytest.NewDischarger(d1) d2.CheckerP = bakerytest.ConditionParser(d2checker) defer d2.Close() locator := bakery.NewThirdPartyStore() locator.AddInfo(d1.Location(), bakery.ThirdPartyInfo{ PublicKey: d1.Key.Public, Version: bakery.LatestVersion, }) locator.AddInfo(d2.Location(), bakery.ThirdPartyInfo{ PublicKey: d2.Key.Public, Version: bakery.LatestVersion, }) b := bakery.New(bakery.BakeryParams{ Location: "here", Locator: locator, Key: bakery.MustGenerateKey(), }) m, err := b.Oven.NewMacaroon(context.Background(), bakery.LatestVersion, []checkers.Caveat{{ Location: d2.Location(), Condition: "true", }}, dischargeOp) c.Assert(err, qt.IsNil) ms, err := client.DischargeAll(context.Background(), m) c.Assert(err, qt.IsNil) c.Assert(ms, qt.HasLen, 3) _, err = b.Checker.Auth(ms).Allow(context.Background(), dischargeOp) c.Assert(err, qt.IsNil) err = b.Oven.AddCaveat(context.Background(), m, checkers.Caveat{ Location: d2.Location(), Condition: "nope", }) c.Assert(err, qt.IsNil) ms, err = client.DischargeAll(context.Background(), m) c.Assert(err, qt.ErrorMatches, `cannot get discharge from "https://[^"]*": third party refused discharge: cannot discharge: caveat refused`) c.Assert(ms, qt.HasLen, 0) } func TestInsecureSkipVerifyRestoration(t *testing.T) { c := qt.New(t) defer func() { http.DefaultTransport.(*http.Transport).TLSClientConfig = nil }() d1 := bakerytest.NewDischarger(nil) d2 := bakerytest.NewDischarger(nil) d2.Close() c.Assert(http.DefaultTransport.(*http.Transport).TLSClientConfig.InsecureSkipVerify, qt.Equals, true) d1.Close() c.Assert(http.DefaultTransport.(*http.Transport).TLSClientConfig.InsecureSkipVerify, qt.Equals, false) // When InsecureSkipVerify is already true, it should not // be restored to false. http.DefaultTransport.(*http.Transport).TLSClientConfig.InsecureSkipVerify = true d3 := bakerytest.NewDischarger(nil) d3.Close() c.Assert(http.DefaultTransport.(*http.Transport).TLSClientConfig.InsecureSkipVerify, qt.Equals, true) } func TestConcurrentDischargers(t *testing.T) { c := qt.New(t) var wg sync.WaitGroup for i := 0; i < 5; i++ { wg.Add(1) go func() { d := bakerytest.NewDischarger(nil) d.Close() wg.Done() }() } wg.Wait() c.Assert(http.DefaultTransport.(*http.Transport).TLSClientConfig.InsecureSkipVerify, qt.Equals, false) } func TestWithGlobalAllowInsecure(t *testing.T) { httpbakery.AllowInsecureThirdPartyLocator = true defer func() { httpbakery.AllowInsecureThirdPartyLocator = false }() TestDischargerSimple(t) } func TestInteractiveDischarger(t *testing.T) { c := qt.New(t) d := bakerytest.NewDischarger(nil) defer d.Close() rendezvous := bakerytest.NewRendezvous() visited := false waited := false d.AddHTTPHandlers(VisitWaitHandlers(VisitWaiter{ Visit: func(p httprequest.Params, dischargeId string) error { visited = true rendezvous.DischargeComplete(dischargeId, []checkers.Caveat{{ Condition: "test pass", }}) return nil }, WaitToken: func(p httprequest.Params, dischargeId string) (*httpbakery.DischargeToken, error) { waited = true _, err := rendezvous.Await(dischargeId, 5*time.Second) if err != nil { return nil, errgo.Mask(err) } return rendezvous.DischargeToken(dischargeId), nil }, })) d.Checker = httpbakery.ThirdPartyCaveatCheckerFunc(func(ctx context.Context, req *http.Request, cav *bakery.ThirdPartyCaveatInfo, token *httpbakery.DischargeToken) ([]checkers.Caveat, error) { if string(cav.Condition) != "something" { return nil, errgo.Newf("wrong condition") } if token != nil { return rendezvous.CheckToken(token, cav) } err := NewVisitWaitError(req, rendezvous.NewDischarge(cav)) return nil, errgo.Mask(err, errgo.Any) }) var r recordingChecker b := bakery.New(bakery.BakeryParams{ Location: "here", Locator: d, Checker: &r, Key: bakery.MustGenerateKey(), }) m, err := b.Oven.NewMacaroon(context.Background(), bakery.LatestVersion, []checkers.Caveat{{ Location: d.Location(), Condition: "something", }}, dischargeOp) c.Assert(err, qt.IsNil) client := httpbakery.NewClient() client.AddInteractor(newTestInteractor()) ms, err := client.DischargeAll(context.Background(), m) c.Assert(err, qt.IsNil) c.Assert(ms, qt.HasLen, 2) _, err = b.Checker.Auth(ms).Allow(context.Background(), dischargeOp) c.Assert(err, qt.IsNil) // First caveat is time-before caveat added by NewMacaroon. // Second is the one added by the discharger above. c.Assert(r.caveats, qt.HasLen, 1) c.Assert(r.caveats[0], qt.Equals, "test pass") c.Check(visited, qt.Equals, true) c.Check(waited, qt.Equals, true) } func TestLoginDischargerError(t *testing.T) { c := qt.New(t) d := bakerytest.NewDischarger(nil) defer d.Close() rendezvous := bakerytest.NewRendezvous() d.AddHTTPHandlers(VisitWaitHandlers(VisitWaiter{ Visit: func(p httprequest.Params, dischargeId string) error { rendezvous.DischargeFailed(dischargeId, errgo.Newf("test error")) return nil }, WaitToken: func(p httprequest.Params, dischargeId string) (*httpbakery.DischargeToken, error) { _, err := rendezvous.Await(dischargeId, 5*time.Second) if err != nil { return nil, errgo.Mask(err) } return nil, errgo.Newf("await succeeded unexpectedly") }, })) d.Checker = httpbakery.ThirdPartyCaveatCheckerFunc(func(ctx context.Context, req *http.Request, cav *bakery.ThirdPartyCaveatInfo, token *httpbakery.DischargeToken) ([]checkers.Caveat, error) { if string(cav.Condition) != "something" { return nil, errgo.Newf("wrong condition") } if token != nil { return nil, errgo.Newf("token received unexpectedly") } err := NewVisitWaitError(req, rendezvous.NewDischarge(cav)) return nil, errgo.Mask(err, errgo.Any) }) b := bakery.New(bakery.BakeryParams{ Location: "here", Locator: d, Key: bakery.MustGenerateKey(), }) m, err := b.Oven.NewMacaroon(context.Background(), bakery.LatestVersion, []checkers.Caveat{{ Location: d.Location(), Condition: "something", }}, dischargeOp) c.Assert(err, qt.IsNil) client := httpbakery.NewClient() client.AddInteractor(newTestInteractor()) _, err = client.DischargeAll(context.Background(), m) c.Assert(err, qt.ErrorMatches, `cannot get discharge from ".*": cannot acquire discharge token: test error`) } func TestInteractiveDischargerRedirection(t *testing.T) { c := qt.New(t) d := bakerytest.NewDischarger(nil) defer d.Close() rendezvous := bakerytest.NewRendezvous() d.AddHTTPHandlers(VisitWaitHandlers(VisitWaiter{ Visit: func(p httprequest.Params, dischargeId string) error { http.Redirect(p.Response, p.Request, d.Location()+"/redirect?dischargeid="+dischargeId, http.StatusFound, ) return nil }, WaitToken: func(p httprequest.Params, dischargeId string) (*httpbakery.DischargeToken, error) { _, err := rendezvous.Await(dischargeId, 5*time.Second) if err != nil { return nil, errgo.Mask(err) } return rendezvous.DischargeToken(dischargeId), nil }, })) d.Mux.GET("/redirect", func(w http.ResponseWriter, req *http.Request, p httprouter.Params) { req.ParseForm() rendezvous.DischargeComplete(req.Form.Get("dischargeid"), []checkers.Caveat{{ Condition: "condition", }}) }) d.Checker = httpbakery.ThirdPartyCaveatCheckerFunc(func(ctx context.Context, req *http.Request, cav *bakery.ThirdPartyCaveatInfo, token *httpbakery.DischargeToken) ([]checkers.Caveat, error) { if string(cav.Condition) != "something" { return nil, errgo.Newf("wrong condition") } if token != nil { return rendezvous.CheckToken(token, cav) } err := NewVisitWaitError(req, rendezvous.NewDischarge(cav)) return nil, errgo.Mask(err, errgo.Any) }) var r recordingChecker b := bakery.New(bakery.BakeryParams{ Location: "here", Locator: d, Key: bakery.MustGenerateKey(), Checker: &r, }) m, err := b.Oven.NewMacaroon(context.Background(), bakery.LatestVersion, []checkers.Caveat{{ Location: d.Location(), Condition: "something", }}, dischargeOp) c.Assert(err, qt.IsNil) client := httpbakery.NewClient() client.AddInteractor(newTestInteractor()) ms, err := client.DischargeAll(context.Background(), m) c.Assert(err, qt.IsNil) c.Assert(ms, qt.HasLen, 2) _, err = b.Checker.Auth(ms).Allow(context.Background(), dischargeOp) c.Assert(err, qt.IsNil) c.Assert(r.caveats, qt.DeepEquals, []string{"condition"}) } type recordingChecker struct { caveats []string } func (c *recordingChecker) CheckFirstPartyCaveat(ctx context.Context, caveat string) error { c.caveats = append(c.caveats, caveat) return nil } func (c *recordingChecker) Namespace() *checkers.Namespace { return nil } func newTestInteractor() httpbakery.WebBrowserInteractor { return httpbakery.WebBrowserInteractor{ OpenWebBrowser: func(u *url.URL) error { resp, err := http.Get(u.String()) if err != nil { return errgo.Mask(err) } resp.Body.Close() if resp.StatusCode != http.StatusOK { return errgo.Newf("unexpected status %q", resp.Status) } return nil }, } } // NewVisitWaitError returns a new interaction-required error // that func NewVisitWaitError(req *http.Request, dischargeId string) *httpbakery.Error { err := httpbakery.NewInteractionRequiredError(nil, req) visitURL := "/visit?dischargeid=" + dischargeId httpbakery.SetWebBrowserInteraction(err, visitURL, "/wait-token?dischargeid="+dischargeId) httpbakery.SetLegacyInteraction(err, visitURL, "/wait?dischargeid="+dischargeId) return err } // VisitWaiter represents a handler for visit-wait interactions. // Each member corresponds to an HTTP endpoint, type VisitWaiter struct { Visit func(p httprequest.Params, dischargeId string) error Wait func(p httprequest.Params, dischargeId string) (*bakery.Macaroon, error) WaitToken func(p httprequest.Params, dischargeId string) (*httpbakery.DischargeToken, error) } var reqServer = httprequest.Server{ ErrorMapper: httpbakery.ErrorToResponse, } func VisitWaitHandlers(vw VisitWaiter) []httprequest.Handler { return reqServer.Handlers(func(p httprequest.Params) (visitWaitHandlers, context.Context, error) { return visitWaitHandlers{vw}, p.Context, nil }) } type visitWaitHandlers struct { vw VisitWaiter } type visitRequest struct { httprequest.Route `httprequest:"GET /visit"` DischargeId string `httprequest:"dischargeid,form"` } func (h visitWaitHandlers) Visit(p httprequest.Params, r *visitRequest) error { if h.vw.Visit == nil { return errgo.Newf("visit not implemented") } return h.vw.Visit(p, r.DischargeId) } type waitTokenRequest struct { httprequest.Route `httprequest:"GET /wait-token"` DischargeId string `httprequest:"dischargeid,form"` } func (h visitWaitHandlers) WaitToken(p httprequest.Params, r *waitTokenRequest) (*httpbakery.WaitTokenResponse, error) { if h.vw.WaitToken == nil { return nil, errgo.Newf("wait-token not implemented") } token, err := h.vw.WaitToken(p, r.DischargeId) if err != nil { return nil, errgo.Mask(err, errgo.Any) } return &httpbakery.WaitTokenResponse{ Kind: token.Kind, Token: string(token.Value), }, nil } type waitRequest struct { httprequest.Route `httprequest:"GET /wait"` DischargeId string `httprequest:"dischargeid,form"` } func (h visitWaitHandlers) Wait(p httprequest.Params, r *waitRequest) (*httpbakery.WaitResponse, error) { if h.vw.Wait == nil { return nil, errgo.Newf("wait not implemented") } m, err := h.vw.Wait(p, r.DischargeId) if err != nil { return nil, errgo.Mask(err, errgo.Any) } return &httpbakery.WaitResponse{ Macaroon: m, }, nil } macaroon-bakery-3.0.2/bakerytest/rendezvous.go000066400000000000000000000112631464427415500215150ustar00rootroot00000000000000package bakerytest import ( "bytes" "fmt" "sync" "time" "gopkg.in/errgo.v1" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/checkers" "github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery" ) // Rendezvous implements a place where discharge information // can be stored, recovered and waited for. type Rendezvous struct { mu sync.Mutex maxId int waiting map[string]*dischargeFuture } func NewRendezvous() *Rendezvous { return &Rendezvous{ waiting: make(map[string]*dischargeFuture), } } type dischargeFuture struct { info *bakery.ThirdPartyCaveatInfo done chan struct{} caveats []checkers.Caveat err error } // NewDischarge creates a new discharge in the rendezvous // associated with the given caveat information. // It returns an identifier for the discharge that can // later be used to complete the discharge or find // out the information again. func (r *Rendezvous) NewDischarge(cav *bakery.ThirdPartyCaveatInfo) string { r.mu.Lock() defer r.mu.Unlock() dischargeId := fmt.Sprintf("%d", r.maxId) r.maxId++ r.waiting[dischargeId] = &dischargeFuture{ info: cav, done: make(chan struct{}), } return dischargeId } // Info returns information on the given discharge id // and reports whether the information has been found. func (r *Rendezvous) Info(dischargeId string) (*bakery.ThirdPartyCaveatInfo, bool) { r.mu.Lock() defer r.mu.Unlock() d := r.waiting[dischargeId] if d == nil { return nil, false } return d.info, true } // DischargeComplete marks the discharge with the given id // as completed with the given caveats, // which will be associated with the given discharge id // and returned from Await. func (r *Rendezvous) DischargeComplete(dischargeId string, caveats []checkers.Caveat) { r.dischargeDone(dischargeId, caveats, nil) } // DischargeFailed marks the discharge with the given id // as failed with the given error, which will be // returned from Await or CheckToken when they're // called with that id. func (r *Rendezvous) DischargeFailed(dischargeId string, err error) { r.dischargeDone(dischargeId, nil, err) } func (r *Rendezvous) dischargeDone(dischargeId string, caveats []checkers.Caveat, err error) { r.mu.Lock() defer r.mu.Unlock() d := r.waiting[dischargeId] if d == nil { panic(errgo.Newf("invalid discharge id %q", dischargeId)) } select { case <-d.done: panic(errgo.Newf("DischargeComplete called twice")) default: } d.caveats, d.err = caveats, err close(d.done) } // Await waits for DischargeComplete or DischargeFailed to be called, // and returns either the caveats passed to DischargeComplete // or the error passed to DischargeFailed. // // It waits for at least the given duration. If timeout is zero, // it returns the information only if it is already available. func (r *Rendezvous) Await(dischargeId string, timeout time.Duration) ([]checkers.Caveat, error) { r.mu.Lock() d := r.waiting[dischargeId] r.mu.Unlock() if d == nil { return nil, errgo.Newf("invalid discharge id %q", dischargeId) } if timeout == 0 { select { case <-d.done: default: return nil, errgo.New("rendezvous has not completed") } } else { select { case <-d.done: case <-time.After(timeout): return nil, errgo.New("timeout waiting for rendezvous to complete") } } if d.err != nil { return nil, errgo.Mask(d.err, errgo.Any) } return d.caveats, nil } func (r *Rendezvous) DischargeToken(dischargeId string) *httpbakery.DischargeToken { _, err := r.Await(dischargeId, 0) if err != nil { panic(errgo.Notef(err, "cannot obtain discharge token for %q", dischargeId)) } return &httpbakery.DischargeToken{ Kind: "discharge-id", Value: []byte(dischargeId), } } // CheckToken checks that the given token is valid for discharging the // given caveat, and returns any caveats passed to DischargeComplete // if it is. func (r *Rendezvous) CheckToken(token *httpbakery.DischargeToken, cav *bakery.ThirdPartyCaveatInfo) ([]checkers.Caveat, error) { if token.Kind != "discharge-id" { return nil, errgo.Newf("invalid discharge token kind %q", token.Kind) } info, ok := r.Info(string(token.Value)) if !ok { return nil, errgo.Newf("discharge token %q not found", token.Value) } if !bytes.Equal(info.Caveat, cav.Caveat) { return nil, errgo.Newf("caveat provided to CheckToken does not match original") } if !bytes.Equal(info.Id, cav.Id) { return nil, errgo.Newf("caveat id provided to CheckToken does not match original") } caveats, err := r.Await(string(token.Value), 0) if err != nil { // Don't mask the error because we want the cause to remain // unchanged if it was passed to DischargeFailed. return nil, errgo.Mask(err, errgo.Any) } return caveats, nil } macaroon-bakery-3.0.2/cmd/000077500000000000000000000000001464427415500153455ustar00rootroot00000000000000macaroon-bakery-3.0.2/cmd/bakery-keygen/000077500000000000000000000000001464427415500201025ustar00rootroot00000000000000macaroon-bakery-3.0.2/cmd/bakery-keygen/go.mod000066400000000000000000000011361464427415500212110ustar00rootroot00000000000000module github.com/go-macaroon-bakery/macaroon-bakery/cmd/bakery-keygen/v3 go 1.21 require github.com/go-macaroon-bakery/macaroon-bakery/v3 v3.0.1 require ( github.com/go-macaroon-bakery/macaroonpb v1.0.0 // indirect github.com/golang/protobuf v1.4.3 // indirect github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af // indirect golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 // indirect golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 // indirect google.golang.org/protobuf v1.25.0 // indirect gopkg.in/errgo.v1 v1.0.1 // indirect gopkg.in/macaroon.v2 v2.1.0 // indirect ) macaroon-bakery-3.0.2/cmd/bakery-keygen/go.sum000066400000000000000000000230611464427415500212370ustar00rootroot00000000000000cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/frankban/quicktest v1.0.0/go.mod h1:R98jIehRai+d1/3Hv2//jOVCTJhW1VBavT6B6CuGq2k= github.com/frankban/quicktest v1.2.2/go.mod h1:Qh/WofXFeiAFII1aEBu529AtJo6Zg2VHscnEsbBnJ20= github.com/frankban/quicktest v1.11.3 h1:8sXhOn0uLys67V8EsXLc6eszDs8VXWxL3iRvebPhedY= github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k= github.com/go-macaroon-bakery/macaroon-bakery/v3 v3.0.1 h1:uvQJoKTHrFFu8zxoaopNKedRzwdy3+8H72we4T/5cGs= github.com/go-macaroon-bakery/macaroon-bakery/v3 v3.0.1/go.mod h1:H59IYeChwvD1po3dhGUPvq5na+4NVD7SJlbhGKvslr0= github.com/go-macaroon-bakery/macaroonpb v1.0.0 h1:It9exBaRMZ9iix1iJ6gwzfwsDE6ExNuwtAJ9e09v6XE= github.com/go-macaroon-bakery/macaroonpb v1.0.0/go.mod h1:UzrGOcbiwTXISFP2XDLDPjfhMINZa+fX/7A2lMd31zc= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.2.1-0.20190312032427-6f77996f0c42/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4 h1:L8R9j+yAqZuZjsqh/z+F1NCffTKKLShY6zXTItVIZ8M= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af h1:gu+uRPtBe88sKxUCEXRoeCvVG90TJmwhiqRpvdhQFng= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= golang.org/x/crypto v0.0.0-20180723164146-c126467f60eb/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 h1:ObdrDkeb4kJdCP557AjRjq69pTHfNouLtWZG7j9rPN8= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 h1:nxC68pudNYkKU6jWhgrqdreuFiOQWj1Fs7T3VrH4Pjw= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= gopkg.in/errgo.v1 v1.0.1 h1:oQFRXzZ7CkBGdm1XZm/EbQYaYNNEElNBOd09M6cqNso= gopkg.in/errgo.v1 v1.0.1/go.mod h1:3NjfXwocQRYAPTq4/fzX+CwUhPRcR/azYRhj8G+LqMo= gopkg.in/macaroon.v2 v2.1.0 h1:HZcsjBCzq9t0eBPMKqTN/uSN6JOm78ZJ2INbqcBQOUI= gopkg.in/macaroon.v2 v2.1.0/go.mod h1:OUb+TQP/OP0WOerC2Jp/3CwhIKyIa9kQjuc7H24e6/o= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= macaroon-bakery-3.0.2/cmd/bakery-keygen/main.go000066400000000000000000000005451464427415500213610ustar00rootroot00000000000000package main import ( "encoding/json" "fmt" "os" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" ) func main() { kp, err := bakery.GenerateKey() if err != nil { fmt.Fprintf(os.Stderr, "cannot generate key: %s\n", err) os.Exit(1) } b, err := json.MarshalIndent(kp, "", "\t") if err != nil { panic(err) } fmt.Printf("%s\n", b) } macaroon-bakery-3.0.2/go.mod000066400000000000000000000026051464427415500157130ustar00rootroot00000000000000module github.com/go-macaroon-bakery/macaroon-bakery/v3 go 1.17 require ( github.com/frankban/quicktest v1.11.3 github.com/go-macaroon-bakery/macaroonpb v1.0.0 github.com/google/go-cmp v0.5.4 github.com/juju/mgo/v2 v2.0.0-20220111072304-f200228f1090 github.com/juju/mgotest v1.0.3 github.com/juju/postgrestest v1.1.0 github.com/juju/qthttptest v0.1.3 github.com/juju/webbrowser v0.0.0-20160309143629-54b8c57083b4 github.com/julienschmidt/httprouter v1.3.0 github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 gopkg.in/errgo.v1 v1.0.1 gopkg.in/httprequest.v1 v1.2.1 gopkg.in/juju/environschema.v1 v1.0.0 gopkg.in/macaroon.v2 v2.1.0 gopkg.in/yaml.v2 v2.4.0 ) require ( github.com/golang/protobuf v1.4.3 // indirect github.com/juju/schema v1.0.0 // indirect github.com/kr/pretty v0.2.1 // indirect github.com/kr/text v0.2.0 // indirect github.com/lib/pq v1.3.0 // indirect github.com/xdg-go/stringprep v1.0.2 // indirect golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 // indirect golang.org/x/text v0.3.5 // indirect golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 // indirect google.golang.org/protobuf v1.25.0 // indirect gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22 // indirect gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect ) macaroon-bakery-3.0.2/go.sum000066400000000000000000000357731464427415500157540ustar00rootroot00000000000000cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/frankban/quicktest v1.0.0/go.mod h1:R98jIehRai+d1/3Hv2//jOVCTJhW1VBavT6B6CuGq2k= github.com/frankban/quicktest v1.1.0/go.mod h1:R98jIehRai+d1/3Hv2//jOVCTJhW1VBavT6B6CuGq2k= github.com/frankban/quicktest v1.2.2/go.mod h1:Qh/WofXFeiAFII1aEBu529AtJo6Zg2VHscnEsbBnJ20= github.com/frankban/quicktest v1.7.2/go.mod h1:jaStnuzAqU1AJdCO0l53JDCJrVDKcS03DbaAcR7Ks/o= github.com/frankban/quicktest v1.10.0/go.mod h1:ui7WezCLWMWxVWr1GETZY3smRy0G4KWq9vcPtJmFl7Y= github.com/frankban/quicktest v1.11.3 h1:8sXhOn0uLys67V8EsXLc6eszDs8VXWxL3iRvebPhedY= github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k= github.com/go-macaroon-bakery/macaroonpb v1.0.0 h1:It9exBaRMZ9iix1iJ6gwzfwsDE6ExNuwtAJ9e09v6XE= github.com/go-macaroon-bakery/macaroonpb v1.0.0/go.mod h1:UzrGOcbiwTXISFP2XDLDPjfhMINZa+fX/7A2lMd31zc= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.2.1-0.20190312032427-6f77996f0c42/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4 h1:L8R9j+yAqZuZjsqh/z+F1NCffTKKLShY6zXTItVIZ8M= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/juju/mgo/v2 v2.0.0-20220111072304-f200228f1090 h1:zX5GoH3Jp8k1EjUFkApu/YZAYEn0PYQfg/U6IDyNyYs= github.com/juju/mgo/v2 v2.0.0-20220111072304-f200228f1090/go.mod h1:N614SE0a4e+ih2rg96Vi2PeC3cTpUOWgCTv3Cgk974c= github.com/juju/mgotest v1.0.3 h1:3UIS2cOSzE6qz/dtiLAaQew5AKYw/bRb++/lsB522HI= github.com/juju/mgotest v1.0.3/go.mod h1:Dnzi6seljG9GoZpqFdTqRV3ybB3UcIj+H8iQqy1so1A= github.com/juju/postgrestest v1.1.0 h1:jEGPSV72rQuQGSzBZfEU15As6FqThPWgNz8ycKD1d1E= github.com/juju/postgrestest v1.1.0/go.mod h1:/n17Y2T6iFozzXwSCO0JYJ5gSiz2caEtSwAjh/uLXDM= github.com/juju/qthttptest v0.1.1/go.mod h1:aTlAv8TYaflIiTDIQYzxnl1QdPjAg8Q8qJMErpKy6A4= github.com/juju/qthttptest v0.1.3 h1:M0HdpwsK/UTHRGRcIw5zvh5z+QOgdqyK+ecDMN+swwM= github.com/juju/qthttptest v0.1.3/go.mod h1:2gayREyVSs/IovPmwYAtU+HZzuhDjytJQRRLzPTtDYE= github.com/juju/schema v1.0.0 h1:sZvJ7iQXHhMw/lJ4YfUmq+fe7R2ZSUzZzd/eSokaB3M= github.com/juju/schema v1.0.0/go.mod h1:Y+ThzXpUJ0E7NYYocAbuvJ7vTivXfrof/IfRPq/0abI= github.com/juju/webbrowser v0.0.0-20160309143629-54b8c57083b4 h1:go1FDIXkFL8AUWgJ7B68rtFWCidyrMfZH9x3xwFK74s= github.com/juju/webbrowser v0.0.0-20160309143629-54b8c57083b4/go.mod h1:G6PCelgkM6cuvyD10iYJsjLBsSadVXtJ+nBxFAxE2BU= github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.3.0 h1:/qkRGz8zljWiDcFvgpwUpwIAPu3r07TDvs3Rws+o/pU= github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af h1:gu+uRPtBe88sKxUCEXRoeCvVG90TJmwhiqRpvdhQFng= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/xdg-go/stringprep v1.0.2 h1:6iq84/ryjjeRmMJwxutI51F2GIPlP5BfTvXHeYjyhBc= github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= golang.org/x/crypto v0.0.0-20180723164146-c126467f60eb/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190404164418-38d8ce5564a5/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 h1:ObdrDkeb4kJdCP557AjRjq69pTHfNouLtWZG7j9rPN8= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200505041828-1ed23360d12c/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 h1:qWPm9rbaAMKs8Bq/9LRpbMqxWRVUAQwMI9fVrssnTfw= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 h1:nxC68pudNYkKU6jWhgrqdreuFiOQWj1Fs7T3VrH4Pjw= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200505023115-26f46d2f7ef8/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v1 v1.0.0/go.mod h1:CxwszS/Xz1C49Ucd2i6Zil5UToP1EmyrFhKaMVbg1mk= gopkg.in/errgo.v1 v1.0.1 h1:oQFRXzZ7CkBGdm1XZm/EbQYaYNNEElNBOd09M6cqNso= gopkg.in/errgo.v1 v1.0.1/go.mod h1:3NjfXwocQRYAPTq4/fzX+CwUhPRcR/azYRhj8G+LqMo= gopkg.in/httprequest.v1 v1.2.1 h1:pEPLMdF/gjWHnKxLpuCYaHFjc8vAB2wrYjXrqDVC16E= gopkg.in/httprequest.v1 v1.2.1/go.mod h1:x2Otw96yda5+8+6ZeWwHIJTFkEHWP/qP8pJOzqEtWPM= gopkg.in/juju/environschema.v1 v1.0.0 h1:51vT1bzbP9fntQ0I9ECSlku2p19Szj/N2beZFeIH2kM= gopkg.in/juju/environschema.v1 v1.0.0/go.mod h1:WTgU3KXKCVoO9bMmG/4KHzoaRvLeoxfjArpgd1MGWFA= gopkg.in/macaroon.v2 v2.1.0 h1:HZcsjBCzq9t0eBPMKqTN/uSN6JOm78ZJ2INbqcBQOUI= gopkg.in/macaroon.v2 v2.1.0/go.mod h1:OUb+TQP/OP0WOerC2Jp/3CwhIKyIa9kQjuc7H24e6/o= gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22 h1:VpOs+IwYnYBaFnrNAeB8UUWtL3vEUnzSCL1nVjPhqrw= gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA= gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637/go.mod h1:BHsqpu/nsuzkT5BpiH1EMZPLyqSMM8JbIavyFACoFNk= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= macaroon-bakery-3.0.2/httpbakery/000077500000000000000000000000001464427415500167575ustar00rootroot00000000000000macaroon-bakery-3.0.2/httpbakery/agent/000077500000000000000000000000001464427415500200555ustar00rootroot00000000000000macaroon-bakery-3.0.2/httpbakery/agent/agent.go000066400000000000000000000170441464427415500215100ustar00rootroot00000000000000// Package agent enables non-interactive (agent) login using macaroons. // To enable agent authorization with a given httpbakery.Client c against // a given third party discharge server URL u: // // SetUpAuth(c, u, agentUsername) // package agent import ( "context" "encoding/json" "errors" "io/ioutil" "net/url" "os" "strings" "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/httpbakery" ) // AuthInfo holds the agent information required // to set up agent authentication information. // It holds the agent's private key and information // about the username associated with each // known agent-authentication server. type AuthInfo struct { Key *bakery.KeyPair `json:"key,omitempty" yaml:"key,omitempty"` Agents []Agent `json:"agents" yaml:"agents"` } // Agent represents an agent that can be used for agent authentication. type Agent struct { // URL holds the URL associated with the agent. URL string `json:"url" yaml:"url"` // Username holds the username to use for the agent. Username string `json:"username" yaml:"username"` } var ErrNoAuthInfo = errgo.New("no bakery agent info found in environment") // AuthInfoFromEnvironment returns an AuthInfo derived // from environment variables. // // It recognizes the following variable: // BAKERY_AGENT_FILE - path to a file containing agent authentication // info in JSON format (as marshaled by the AuthInfo type). // // If BAKERY_AGENT_FILE is not set, ErrNoAuthInfo will be returned. func AuthInfoFromEnvironment() (*AuthInfo, error) { agentFile := os.Getenv("BAKERY_AGENT_FILE") if agentFile == "" { return nil, errgo.WithCausef(nil, ErrNoAuthInfo, "") } var ai AuthInfo data, err := ioutil.ReadFile(agentFile) if err != nil { return nil, errgo.Mask(err) } if err := json.Unmarshal(data, &ai); err != nil { return nil, errgo.Notef(err, "cannot unmarshal agent information from %q: %v", agentFile, err) } if ai.Key == nil { return nil, errgo.Newf("no private key found in %q", agentFile) } return &ai, nil } // SetUpAuth sets up agent authentication on the given client. // If this is called several times on the same client, earlier // calls will take precedence over later calls when there's // a URL and username match for both. func SetUpAuth(client *httpbakery.Client, authInfo *AuthInfo) error { if authInfo.Key == nil { return errgo.Newf("no key in auth info") } if client.Key != nil { if *client.Key != *authInfo.Key { return errgo.Newf("client already has a different key set up") } } else { client.Key = authInfo.Key } client.AddInteractor(interactor{authInfo}) return nil } // InteractionInfo holds the information expected in // the agent interaction entry in an interaction-required // error. type InteractionInfo struct { // LoginURL holds the URL from which to acquire // a macaroon that can be used to complete the agent // login. To acquire the macaroon, make a POST // request to the URL with user and public-key // parameters. LoginURL string `json:"login-url"` } // SetInteraction sets agent interaction information on the // given error, which should be an interaction-required // error to be returned from a discharge request. // // The given URL (which may be relative to the discharger // location) will be the subject of a GET request by the // client to fetch the agent macaroon that, when discharged, // can act as the discharge token. func SetInteraction(e *httpbakery.Error, loginURL string) { e.SetInteraction("agent", &InteractionInfo{ LoginURL: loginURL, }) } // interactor is a httpbakery.Interactor that performs interaction using the // agent login protocol. type interactor struct { authInfo *AuthInfo } func (i interactor) Kind() string { return "agent" } // agentMacaroonRequest represents a request to get the // agent macaroon that, when discharged, becomes // the discharge token to complete the discharge. type agentMacaroonRequest struct { httprequest.Route `httprequest:"GET"` Username string `httprequest:"username,form"` PublicKey *bakery.PublicKey `httprequest:"public-key,form"` } type agentMacaroonResponse struct { Macaroon *bakery.Macaroon `json:"macaroon"` } func (i interactor) Interact(ctx context.Context, client *httpbakery.Client, location string, interactionRequiredErr *httpbakery.Error) (*httpbakery.DischargeToken, error) { var p InteractionInfo err := interactionRequiredErr.InteractionMethod("agent", &p) if err != nil { return nil, errgo.Mask(err) } if p.LoginURL == "" { return nil, errgo.Newf("no login-url field found in agent interaction method") } agent, err := i.findAgent(location) if err != nil { return nil, errgo.Mask(err) } loginURL, err := relativeURL(location, p.LoginURL) if err != nil { return nil, errgo.Mask(err) } var resp agentMacaroonResponse err = (&httprequest.Client{ Doer: client, }).CallURL(ctx, loginURL.String(), &agentMacaroonRequest{ Username: agent.Username, PublicKey: &client.Key.Public, }, &resp) if err != nil { return nil, errgo.Notef(err, "cannot acquire agent macaroon") } if resp.Macaroon == nil { return nil, errgo.Newf("no macaroon in response") } ms, err := client.DischargeAll(ctx, resp.Macaroon) if err != nil { return nil, errgo.Notef(err, "cannot discharge agent macaroon") } data, err := ms.MarshalBinary() if err != nil { return nil, errgo.Notef(err, "cannot marshal agent macaroon") } return &httpbakery.DischargeToken{ Kind: "agent", Value: data, }, nil } // findAgent finds an appropriate agent entry // for the given location. func (i interactor) findAgent(location string) (*Agent, error) { for _, a := range i.authInfo.Agents { // Don't worry about trailing slashes if strings.TrimSuffix(a.URL, "/") == strings.TrimSuffix(location, "/") { return &a, nil } } return nil, errgo.WithCausef(nil, httpbakery.ErrInteractionMethodNotFound, "cannot find username for discharge location %q", location) } type agentLoginRequest struct { httprequest.Route `httprequest:"POST"` Body LegacyAgentLoginBody `httprequest:",body"` } // LegacyAgentLoginBody is used to encode the JSON body // sent when making a legacy agent protocol // POST request to the visit URL. type LegacyAgentLoginBody struct { Username string `json:"username"` PublicKey *bakery.PublicKey `json:"public_key"` } // LegacyAgentResponse contains the response to a // legacy agent login attempt. type LegacyAgentResponse struct { AgentLogin bool `json:"agent_login"` } // LegacyInteract implements httpbakery.LegactInteractor.LegacyInteract. func (i interactor) LegacyInteract(ctx context.Context, client *httpbakery.Client, location string, visitURL *url.URL) error { c := &httprequest.Client{ Doer: client, } agent, err := i.findAgent(location) if err != nil { return errgo.Mask(err) } var resp LegacyAgentResponse err = c.CallURL(ctx, visitURL.String(), &agentLoginRequest{ Body: LegacyAgentLoginBody{ Username: agent.Username, PublicKey: &client.Key.Public, }, }, &resp) if err != nil { return errgo.Mask(err) } if !resp.AgentLogin { return errors.New("agent login failed") } return nil } // relativeURL returns newPath relative to an original URL. func relativeURL(base, new string) (*url.URL, error) { if new == "" { return nil, errgo.Newf("empty URL") } baseURL, err := url.Parse(base) if err != nil { return nil, errgo.Notef(err, "cannot parse URL") } newURL, err := url.Parse(new) if err != nil { return nil, errgo.Notef(err, "cannot parse URL") } return baseURL.ResolveReference(newURL), nil } macaroon-bakery-3.0.2/httpbakery/agent/agent_test.go000066400000000000000000000133051464427415500225430ustar00rootroot00000000000000package agent_test import ( "context" "encoding/json" "io/ioutil" "net/http" "os" "testing" qt "github.com/frankban/quicktest" "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" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakerytest" "github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery" "github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery/agent" ) var agentLoginOp = bakery.Op{"agent", "login"} func TestSetUpAuth(t *testing.T) { c := qt.New(t) defer c.Done() f := newAgentFixture(c) dischargerBakery := bakery.New(bakery.BakeryParams{ Key: f.discharger.Key, }) f.discharger.AddHTTPHandlers(AgentHandlers(AgentHandler{ AgentMacaroon: func(p httprequest.Params, username string, pubKey *bakery.PublicKey) (*bakery.Macaroon, error) { if username != "test-user" || *pubKey != f.agentBakery.Oven.Key().Public { return nil, errgo.Newf("mismatched user/pubkey; want %s got %s", f.agentBakery.Oven.Key().Public, *pubKey) } version := httpbakery.RequestVersion(p.Request) return dischargerBakery.Oven.NewMacaroon( context.Background(), bakery.LatestVersion, []checkers.Caveat{ bakery.LocalThirdPartyCaveat(pubKey, version), }, agentLoginOp, ) }, })) f.discharger.Checker = httpbakery.ThirdPartyCaveatCheckerFunc(func(ctx context.Context, req *http.Request, info *bakery.ThirdPartyCaveatInfo, token *httpbakery.DischargeToken) ([]checkers.Caveat, error) { if token != nil { c.Logf("with token request: %v", req.URL) if token.Kind != "agent" { return nil, errgo.Newf("unexpected discharge token kind %q", token.Kind) } var m macaroon.Slice if err := m.UnmarshalBinary(token.Value); err != nil { return nil, errgo.Notef(err, "cannot unmarshal token") } if _, err := dischargerBakery.Checker.Auth(m).Allow(ctx, agentLoginOp); err != nil { return nil, errgo.Newf("received unexpected discharge token") } return nil, nil } if string(info.Condition) != "some-third-party-caveat" { return nil, errgo.Newf("unexpected caveat condition") } err := httpbakery.NewInteractionRequiredError(nil, req) agent.SetInteraction(err, "/agent-macaroon") return nil, err }) client := httpbakery.NewClient() err := agent.SetUpAuth(client, &agent.AuthInfo{ Key: f.agentBakery.Oven.Key(), Agents: []agent.Agent{{ URL: f.discharger.Location(), Username: "test-user", }}, }) someOp := bakery.Op{ Entity: "something", Action: "doit", } c.Assert(err, qt.IsNil) m, err := f.serverBakery.Oven.NewMacaroon( context.Background(), bakery.LatestVersion, []checkers.Caveat{{ Location: f.discharger.Location(), Condition: "some-third-party-caveat", }}, someOp, ) c.Assert(err, qt.Equals, nil) ms, err := client.DischargeAll(context.Background(), m) c.Assert(err, qt.Equals, nil) _, err = f.serverBakery.Checker.Auth(ms).Allow(context.Background(), someOp) c.Assert(err, qt.Equals, nil) } func TestAuthInfoFromEnvironment(t *testing.T) { c := qt.New(t) defer c.Done() defer os.Setenv("BAKERY_AGENT_FILE", "") f, err := ioutil.TempFile("", "") c.Assert(err, qt.Equals, nil) defer os.Remove(f.Name()) defer f.Close() authInfo := &agent.AuthInfo{ Key: bakery.MustGenerateKey(), Agents: []agent.Agent{{ URL: "https://0.1.2.3/x", Username: "bob", }, { URL: "https://0.2.3.4", Username: "charlie", }}, } data, err := json.Marshal(authInfo) _, err = f.Write(data) c.Assert(err, qt.Equals, nil) f.Close() os.Setenv("BAKERY_AGENT_FILE", f.Name()) authInfo1, err := agent.AuthInfoFromEnvironment() c.Assert(err, qt.Equals, nil) c.Assert(authInfo1, qt.DeepEquals, authInfo) } func TestAuthInfoFromEnvironmentNotSet(t *testing.T) { c := qt.New(t) defer c.Done() os.Setenv("BAKERY_AGENT_FILE", "") authInfo, err := agent.AuthInfoFromEnvironment() c.Assert(errgo.Cause(err), qt.Equals, agent.ErrNoAuthInfo) c.Assert(authInfo, qt.IsNil) } type agentFixture struct { agentBakery *bakery.Bakery serverBakery *bakery.Bakery discharger *bakerytest.Discharger } func newAgentFixture(c *qt.C) *agentFixture { var f agentFixture f.discharger = bakerytest.NewDischarger(nil) c.Defer(f.discharger.Close) f.agentBakery = bakery.New(bakery.BakeryParams{ Key: bakery.MustGenerateKey(), }) f.serverBakery = bakery.New(bakery.BakeryParams{ Locator: f.discharger, Key: bakery.MustGenerateKey(), }) return &f } func AgentHandlers(h AgentHandler) []httprequest.Handler { return reqServer.Handlers(func(p httprequest.Params) (agentHandlers, context.Context, error) { return agentHandlers{h}, p.Context, nil }) } // AgentHandler holds the functions that may be called by the // agent-interaction server. type AgentHandler struct { AgentMacaroon func(p httprequest.Params, username string, pubKey *bakery.PublicKey) (*bakery.Macaroon, error) } // agentHandlers is used to define the handler methods. type agentHandlers struct { h AgentHandler } // agentMacaroonRequest represents a request for the // agent macaroon - it matches agent.agentMacaroonRequest. type agentMacaroonRequest struct { httprequest.Route `httprequest:"GET /agent-macaroon"` Username string `httprequest:"username,form"` PublicKey *bakery.PublicKey `httprequest:"public-key,form"` } type agentMacaroonResponse struct { Macaroon *bakery.Macaroon `json:"macaroon"` } func (h agentHandlers) AgentMacaroon(p httprequest.Params, r *agentMacaroonRequest) (*agentMacaroonResponse, error) { m, err := h.h.AgentMacaroon(p, r.Username, r.PublicKey) if err != nil { return nil, errgo.Mask(err, errgo.Any) } return &agentMacaroonResponse{ Macaroon: m, }, nil } macaroon-bakery-3.0.2/httpbakery/agent/cookie.go000066400000000000000000000027451464427415500216650ustar00rootroot00000000000000package agent import ( "encoding/json" "net/http" "gopkg.in/errgo.v1" "gopkg.in/macaroon.v2" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" ) const cookieName = "agent-login" // agentLogin defines the structure of an agent login cookie. type agentLogin struct { Username string `json:"username"` PublicKey *bakery.PublicKey `json:"public_key"` } // ErrNoAgentLoginCookie is the error returned when the expected // agent login cookie has not been found. var ErrNoAgentLoginCookie = errgo.New("no agent-login cookie found") // LoginCookie returns details of the agent login cookie // from the given request. If no agent-login cookie is found, // it returns an ErrNoAgentLoginCookie error. // // This function is only applicable to the legacy agent // protocol and will be deprecated in the future. func LoginCookie(req *http.Request) (username string, key *bakery.PublicKey, err error) { c, err := req.Cookie(cookieName) if err != nil { return "", nil, ErrNoAgentLoginCookie } b, err := macaroon.Base64Decode([]byte(c.Value)) if err != nil { return "", nil, errgo.Notef(err, "cannot decode cookie value") } var al agentLogin if err := json.Unmarshal(b, &al); err != nil { return "", nil, errgo.Notef(err, "cannot unmarshal agent login") } if al.Username == "" { return "", nil, errgo.Newf("agent login has no user name") } if al.PublicKey == nil { return "", nil, errgo.Newf("agent login has no public key") } return al.Username, al.PublicKey, nil } macaroon-bakery-3.0.2/httpbakery/agent/cookie_test.go000066400000000000000000000055671464427415500227310ustar00rootroot00000000000000package agent_test import ( "encoding/base64" "encoding/json" "net/http" "testing" qt "github.com/frankban/quicktest" "gopkg.in/errgo.v1" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" "github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery/agent" ) var loginCookieTests = []struct { about string addCookie func(*http.Request, *bakery.PublicKey) expectUser string expectError string expectCause error }{{ about: "success", addCookie: func(req *http.Request, key *bakery.PublicKey) { addCookie(req, "bob", key) }, expectUser: "bob", }, { about: "no cookie", addCookie: func(req *http.Request, key *bakery.PublicKey) {}, expectError: "no agent-login cookie found", expectCause: agent.ErrNoAgentLoginCookie, }, { about: "invalid base64 encoding", addCookie: func(req *http.Request, key *bakery.PublicKey) { req.AddCookie(&http.Cookie{ Name: "agent-login", Value: "x", }) }, expectError: "cannot decode cookie value: illegal base64 data at input byte 0", }, { about: "invalid JSON", addCookie: func(req *http.Request, key *bakery.PublicKey) { req.AddCookie(&http.Cookie{ Name: "agent-login", Value: base64.StdEncoding.EncodeToString([]byte("}")), }) }, expectError: "cannot unmarshal agent login: invalid character '}' looking for beginning of value", }, { about: "no username", addCookie: func(req *http.Request, key *bakery.PublicKey) { addCookie(req, "", key) }, expectError: "agent login has no user name", }, { about: "no public key", addCookie: func(req *http.Request, key *bakery.PublicKey) { addCookie(req, "bob", nil) }, expectError: "agent login has no public key", }} func TestLoginCookie(t *testing.T) { c := qt.New(t) key, err := bakery.GenerateKey() c.Assert(err, qt.IsNil) for i, test := range loginCookieTests { c.Logf("test %d: %s", i, test.about) req, err := http.NewRequest("GET", "", nil) c.Assert(err, qt.IsNil) test.addCookie(req, &key.Public) gotUsername, gotKey, err := agent.LoginCookie(req) if test.expectError != "" { c.Assert(err, qt.ErrorMatches, test.expectError) if test.expectCause != nil { c.Assert(errgo.Cause(err), qt.Equals, test.expectCause) } continue } c.Assert(gotUsername, qt.Equals, test.expectUser) c.Assert(gotKey, qt.DeepEquals, &key.Public) } } // addCookie adds an agent-login cookie with the specified parameters to // the given request. func addCookie(req *http.Request, username string, key *bakery.PublicKey) { al := agent.AgentLogin{ Username: username, PublicKey: key, } data, err := json.Marshal(al) if err != nil { // This should be impossible as the agentLogin structure // has to be marshalable. It is certainly a bug if it // isn't. panic(errgo.Notef(err, "cannot marshal %s cookie", agent.CookieName)) } req.AddCookie(&http.Cookie{ Name: agent.CookieName, Value: base64.StdEncoding.EncodeToString(data), }) } macaroon-bakery-3.0.2/httpbakery/agent/example_test.go000066400000000000000000000011551464427415500231000ustar00rootroot00000000000000package agent_test import ( "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" "github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery" "github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery/agent" ) func ExampleSetUpAuth() { // In practice the key would be read from persistent // storage. key, err := bakery.GenerateKey() if err != nil { // handle error } client := httpbakery.NewClient() err = agent.SetUpAuth(client, &agent.AuthInfo{ Key: key, Agents: []agent.Agent{{ URL: "http://foo.com", Username: "agent-username", }}, }) if err != nil { // handle error } } macaroon-bakery-3.0.2/httpbakery/agent/export_test.go000066400000000000000000000001111464427415500227550ustar00rootroot00000000000000package agent type AgentLogin agentLogin const CookieName = cookieName macaroon-bakery-3.0.2/httpbakery/agent/legacy_test.go000066400000000000000000000277741464427415500227300ustar00rootroot00000000000000package agent_test import ( "context" "net/http" "testing" "time" qt "github.com/frankban/quicktest" "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/bakery/checkers" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/identchecker" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakerytest" "github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery" "github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery/agent" ) type visitFunc func(w http.ResponseWriter, req *http.Request, dischargeId string) error type agentPostFunc func(httprequest.Params, agentPostRequest) error var legacyAgentLoginErrorTests = []struct { about string visitHandler visitFunc agentPostHandler agentPostFunc expectError string }{{ about: "error response", agentPostHandler: func(httprequest.Params, agentPostRequest) error { return errgo.Newf("test error") }, expectError: `cannot get discharge from ".*": cannot start interactive session: Post http(s)?://.*: test error`, }, { about: "unexpected response", agentPostHandler: func(p httprequest.Params, _ agentPostRequest) error { p.Response.Write([]byte("OK")) return nil }, expectError: `cannot get discharge from ".*": cannot start interactive session: Post http(s)?://.*: unexpected content type text/plain; want application/json; content: OK`, }, { about: "unexpected error response", agentPostHandler: func(p httprequest.Params, _ agentPostRequest) error { httprequest.WriteJSON(p.Response, http.StatusBadRequest, httpbakery.Error{}) return nil }, expectError: `cannot get discharge from ".*": cannot start interactive session: Post http(s)?://.*: no error message found`, }, { about: "login false value", agentPostHandler: func(p httprequest.Params, _ agentPostRequest) error { httprequest.WriteJSON(p.Response, http.StatusOK, agent.LegacyAgentResponse{}) return nil }, expectError: `cannot get discharge from ".*": cannot start interactive session: agent login failed`, }} func TestAgentLoginError(t *testing.T) { c := qt.New(t) for _, test := range legacyAgentLoginErrorTests { c.Run(test.about, func(c *qt.C) { f := newLegacyAgentFixture(c) var agentPost agentPostFunc f.discharger.AddHTTPHandlers(newLegacyAgentHandlers(legacyAgentHandler{ agentPost: func(p httprequest.Params, r agentPostRequest) error { return agentPost(p, r) }, })) rendezvous := bakerytest.NewRendezvous() f.discharger.Checker = httpbakery.ThirdPartyCaveatCheckerFunc(func(ctx context.Context, req *http.Request, info *bakery.ThirdPartyCaveatInfo, token *httpbakery.DischargeToken) ([]checkers.Caveat, error) { if token != nil { return nil, errgo.Newf("received unexpected discharge token") } dischargeId := rendezvous.NewDischarge(info) err := httpbakery.NewInteractionRequiredError(nil, req) err.Info = &httpbakery.ErrorInfo{ LegacyVisitURL: "/visit?dischargeid=" + dischargeId, LegacyWaitURL: "/wait?dischargeid=" + dischargeId, } return nil, err }) agentPost = test.agentPostHandler client := httpbakery.NewClient() err := agent.SetUpAuth(client, &agent.AuthInfo{ Key: f.agentBakery.Oven.Key(), Agents: []agent.Agent{{ URL: f.discharger.Location(), Username: "test-user", }}, }) c.Assert(err, qt.IsNil) m, err := f.serverBakery.Oven.NewMacaroon( context.Background(), bakery.LatestVersion, identityCaveats(f.discharger.Location()), identchecker.LoginOp, ) c.Assert(err, qt.IsNil) ms, err := client.DischargeAll(context.Background(), m) c.Assert(err, qt.ErrorMatches, test.expectError) c.Assert(ms, qt.IsNil) }) } } func TestLegacySetUpAuth(t *testing.T) { c := qt.New(t) defer c.Done() f := newLegacyAgentFixture(c) rendezvous := bakerytest.NewRendezvous() f.discharger.AddHTTPHandlers(newLegacyAgentHandlers(legacyAgentHandler{ agentPost: func(p httprequest.Params, r agentPostRequest) error { return f.agentPost(p, r, rendezvous) }, wait: func(p httprequest.Params, dischargeId string) (*bakery.Macaroon, error) { caveats, err := rendezvous.Await(dischargeId, 5*time.Second) if err != nil { return nil, errgo.Mask(err) } info, _ := rendezvous.Info(dischargeId) return f.discharger.DischargeMacaroon(p.Context, info, caveats) }, })) f.discharger.Checker = httpbakery.ThirdPartyCaveatCheckerFunc(func(ctx context.Context, req *http.Request, info *bakery.ThirdPartyCaveatInfo, token *httpbakery.DischargeToken) ([]checkers.Caveat, error) { if token != nil { return nil, errgo.Newf("received unexpected discharge token") } dischargeId := rendezvous.NewDischarge(info) err := httpbakery.NewInteractionRequiredError(nil, req) err.Info = &httpbakery.ErrorInfo{ LegacyVisitURL: "/visit?dischargeid=" + dischargeId, LegacyWaitURL: "/wait?dischargeid=" + dischargeId, } return nil, err }) client := httpbakery.NewClient() err := agent.SetUpAuth(client, &agent.AuthInfo{ Key: f.agentBakery.Oven.Key(), Agents: []agent.Agent{{ URL: f.discharger.Location(), Username: "test-user", }}, }) c.Assert(err, qt.IsNil) m, err := f.serverBakery.Oven.NewMacaroon( context.Background(), bakery.LatestVersion, identityCaveats(f.discharger.Location()), identchecker.LoginOp, ) c.Assert(err, qt.IsNil) ms, err := client.DischargeAll(context.Background(), m) c.Assert(err, qt.IsNil) authInfo, err := f.serverBakery.Checker.Auth(ms).Allow(context.Background(), identchecker.LoginOp) c.Assert(err, qt.IsNil) c.Assert(authInfo.Identity, qt.Equals, identchecker.SimpleIdentity("test-user")) } func TestLegacyNoMatchingSite(t *testing.T) { c := qt.New(t) defer c.Done() f := newLegacyAgentFixture(c) rendezvous := bakerytest.NewRendezvous() f.discharger.AddHTTPHandlers(newLegacyAgentHandlers(legacyAgentHandler{ agentPost: func(p httprequest.Params, r agentPostRequest) error { return f.agentPost(p, r, rendezvous) }, wait: func(p httprequest.Params, dischargeId string) (*bakery.Macaroon, error) { _, err := rendezvous.Await(dischargeId, 5*time.Second) if err != nil { return nil, errgo.Mask(err) } return nil, errgo.Newf("rendezvous unexpectedly succeeded") }, })) f.discharger.Checker = httpbakery.ThirdPartyCaveatCheckerFunc(func(ctx context.Context, req *http.Request, info *bakery.ThirdPartyCaveatInfo, token *httpbakery.DischargeToken) ([]checkers.Caveat, error) { if token != nil { return nil, errgo.Newf("received unexpected discharge token") } dischargeId := rendezvous.NewDischarge(info) err := httpbakery.NewInteractionRequiredError(nil, req) err.Info = &httpbakery.ErrorInfo{ LegacyVisitURL: "/visit?dischargeid=" + dischargeId, LegacyWaitURL: "/wait?dischargeid=" + dischargeId, } return nil, err }) client := httpbakery.NewClient() err := agent.SetUpAuth(client, &agent.AuthInfo{ Key: bakery.MustGenerateKey(), Agents: []agent.Agent{{ URL: "http://0.1.2.3/", Username: "test-user", }}, }) c.Assert(err, qt.IsNil) m, err := f.serverBakery.Oven.NewMacaroon( context.Background(), bakery.LatestVersion, identityCaveats(f.discharger.Location()), identchecker.LoginOp, ) c.Assert(err, qt.IsNil) _, err = client.DischargeAll(context.Background(), m) c.Assert(err, qt.ErrorMatches, `cannot get discharge from ".*": cannot start interactive session: cannot find username for discharge location ".*"`) _, ok := errgo.Cause(err).(*httpbakery.InteractionError) c.Assert(ok, qt.Equals, true) } type idmClient struct { dischargerURL string } func (c idmClient) IdentityFromContext(ctx context.Context) (identchecker.Identity, []checkers.Caveat, error) { return nil, identityCaveats(c.dischargerURL), nil } func identityCaveats(dischargerURL string) []checkers.Caveat { return []checkers.Caveat{{ Location: dischargerURL, Condition: "test condition", }} } func (c idmClient) DeclaredIdentity(ctx context.Context, declared map[string]string) (identchecker.Identity, error) { return identchecker.SimpleIdentity(declared["username"]), nil } func (f *legacyAgentFixture) agentPost(p httprequest.Params, r agentPostRequest, rendezvous *bakerytest.Rendezvous) error { ctx := context.TODO() if r.Body.Username == "" || r.Body.PublicKey == nil { return errgo.Newf("username or public key not found") } authInfo, authErr := f.agentBakery.Checker.Auth(httpbakery.RequestMacaroons(p.Request)...).Allow(ctx, identchecker.LoginOp) if authErr == nil && authInfo.Identity != nil { rendezvous.DischargeComplete(r.DischargeId, []checkers.Caveat{ checkers.DeclaredCaveat("username", authInfo.Identity.Id()), }) httprequest.WriteJSON(p.Response, http.StatusOK, agent.LegacyAgentResponse{true}) return nil } version := httpbakery.RequestVersion(p.Request) m, err := f.agentBakery.Oven.NewMacaroon(ctx, version, []checkers.Caveat{ bakery.LocalThirdPartyCaveat(r.Body.PublicKey, version), checkers.DeclaredCaveat("username", r.Body.Username), }, identchecker.LoginOp) if err != nil { return errgo.Notef(err, "cannot create macaroon") } return httpbakery.NewDischargeRequiredError(httpbakery.DischargeRequiredErrorParams{ Macaroon: m, Request: p.Request, }) } type legacyAgentFixture struct { agentBakery *identchecker.Bakery serverBakery *identchecker.Bakery discharger *bakerytest.Discharger } func newLegacyAgentFixture(c *qt.C) *legacyAgentFixture { var f legacyAgentFixture f.discharger = bakerytest.NewDischarger(nil) c.Defer(f.discharger.Close) f.agentBakery = identchecker.NewBakery(identchecker.BakeryParams{ IdentityClient: idmClient{f.discharger.Location()}, Key: bakery.MustGenerateKey(), }) f.serverBakery = identchecker.NewBakery(identchecker.BakeryParams{ Locator: f.discharger, IdentityClient: idmClient{f.discharger.Location()}, Key: bakery.MustGenerateKey(), }) return &f } // legacyAgentHandler represents a handler for legacy // agent interactions. Each member corresponds to an HTTP endpoint, type legacyAgentHandler struct { agentPost agentPostFunc wait func(p httprequest.Params, dischargeId string) (*bakery.Macaroon, error) } var reqServer = httprequest.Server{ ErrorMapper: httpbakery.ErrorToResponse, } func newLegacyAgentHandlers(h legacyAgentHandler) []httprequest.Handler { return reqServer.Handlers(func(p httprequest.Params) (legacyAgentHandlers, context.Context, error) { return legacyAgentHandlers{h}, p.Context, nil }) } type legacyAgentHandlers struct { h legacyAgentHandler } type visitGetRequest struct { httprequest.Route `httprequest:"GET /visit"` DischargeId string `httprequest:"dischargeid,form"` } func (h legacyAgentHandlers) VisitGet(p httprequest.Params, r *visitGetRequest) error { return handleLoginMethods(p, r.DischargeId) } // handleLoginMethods handles a legacy visit request // to ask for the set of login methods. // It reports whether it has handled the request. func handleLoginMethods(p httprequest.Params, dischargeId string) error { if p.Request.Header.Get("Accept") != "application/json" { return errgo.Newf("got normal visit request") } httprequest.WriteJSON(p.Response, http.StatusOK, map[string]string{ "agent": "/agent?discharge-id=" + dischargeId, }) return nil } type agentPostRequest struct { httprequest.Route `httprequest:"POST /agent"` DischargeId string `httprequest:"discharge-id,form"` Body agent.LegacyAgentLoginBody `httprequest:",body"` } func (h legacyAgentHandlers) AgentPost(p httprequest.Params, r *agentPostRequest) error { if h.h.agentPost == nil { return errgo.Newf("agent POST not implemented") } return h.h.agentPost(p, *r) } type waitRequest struct { httprequest.Route `httprequest:"GET /wait"` DischargeId string `httprequest:"dischargeid,form"` } func (h legacyAgentHandlers) Wait(p httprequest.Params, r *waitRequest) (*httpbakery.WaitResponse, error) { if h.h.wait == nil { return nil, errgo.Newf("wait not implemented") } m, err := h.h.wait(p, r.DischargeId) if err != nil { return nil, errgo.Mask(err, errgo.Any) } return &httpbakery.WaitResponse{ Macaroon: m, }, nil } macaroon-bakery-3.0.2/httpbakery/agent/protocol.go000066400000000000000000000043031464427415500222450ustar00rootroot00000000000000package agent /* PROTOCOL The agent protocol is initiated when attempting to perform a discharge. It works as follows: A is Agent L is Login Service A->L POST /discharge L->A Interaction-required error containing an entry for "agent" with field "login-url" holding URL $loginURL. A->L GET $loginURL?username=$user&public-key=$pubkey where $user is the username to log in as and $pubkey is the public key of that username, base-64 encoded. L->A JSON response: macaroon: macaroon with "local" third-party-caveat addressed to $pubkey. A->L POST /discharge?token-kind=agent&token64=self-discharged macaroon The macaroon is binary-encoded, then base64 encoded. Note that, as with most Go HTTP handlers, the parameters may also be in the form-encoded request body. L->A discharge macaroon A local third-party caveat is a third party caveat with the location set to "local" and the caveat encrypted with the public key specified in the GET request. LEGACY PROTOCOL The legacy agent protocol is used by services that don't yet implement the new protocol. Once a discharge has failed with an interaction required error, an agent login works as follows: Agent Login Service | | | GET visitURL with agent cookie | |----------------------------------->| | | | Macaroon with local third-party | | caveat | |<-----------------------------------| | | | GET visitURL with agent cookie & | | discharged macaroon | |----------------------------------->| | | | Agent login response | |<-----------------------------------| | | The agent cookie is a cookie in the same form described in the PROTOCOL section above. On success the response is the following JSON object: { "agent_login": "true" } If an error occurs then the response should be a JSON object that unmarshals to an httpbakery.Error. */ macaroon-bakery-3.0.2/httpbakery/browser.go000066400000000000000000000146361464427415500210030ustar00rootroot00000000000000package httpbakery import ( "context" "fmt" "net/http" "net/url" "os" "github.com/juju/webbrowser" "gopkg.in/errgo.v1" "gopkg.in/httprequest.v1" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" ) const WebBrowserInteractionKind = "browser-window" // WaitTokenResponse holds the response type // returned, JSON-encoded, from the waitToken // URL passed to SetBrowserInteraction. type WaitTokenResponse struct { Kind string `json:"kind"` // Token holds the token value when it's well-formed utf-8 Token string `json:"token,omitempty"` // Token64 holds the token value, base64 encoded, when it's // not well-formed utf-8. Token64 string `json:"token64,omitempty"` } // WaitResponse holds the type that should be returned // by an HTTP response made to a LegacyWaitURL // (See the ErrorInfo type). type WaitResponse struct { Macaroon *bakery.Macaroon } // WebBrowserInteractionInfo holds the information // expected in the browser-window interaction // entry in an interaction-required error. type WebBrowserInteractionInfo struct { // VisitURL holds the URL to be visited in a web browser. VisitURL string // WaitTokenURL holds a URL that will block on GET // until the browser interaction has completed. // On success, the response is expected to hold a waitTokenResponse // in its body holding the token to be returned from the // Interact method. WaitTokenURL string } var ( _ Interactor = WebBrowserInteractor{} _ LegacyInteractor = WebBrowserInteractor{} ) // OpenWebBrowser opens a web browser at the // given URL. If the OS is not recognised, the URL // is just printed to standard output. func OpenWebBrowser(url *url.URL) error { err := webbrowser.Open(url) if err == nil { fmt.Fprintf(os.Stderr, "Opening an authorization web page in your browser.\n") fmt.Fprintf(os.Stderr, "If it does not open, please open this URL:\n%s\n", url) return nil } if err == webbrowser.ErrNoBrowser { fmt.Fprintf(os.Stderr, "Please open this URL in your browser to authorize:\n%s\n", url) return nil } return err } // SetWebBrowserInteraction adds information about web-browser-based // interaction to the given error, which should be an // interaction-required error that's about to be returned from a // discharge request. // // The visitURL parameter holds a URL that should be visited by the user // in a web browser; the waitTokenURL parameter holds a URL that can be // long-polled to acquire the resulting discharge token. // // Use SetLegacyInteraction to add support for legacy clients // that don't understand the newer InteractionMethods field. func SetWebBrowserInteraction(e *Error, visitURL, waitTokenURL string) { e.SetInteraction(WebBrowserInteractionKind, WebBrowserInteractionInfo{ VisitURL: visitURL, WaitTokenURL: waitTokenURL, }) } // SetLegacyInteraction adds information about web-browser-based // interaction (or other kinds of legacy-protocol interaction) to the // given error, which should be an interaction-required error that's // about to be returned from a discharge request. // // The visitURL parameter holds a URL that should be visited by the user // in a web browser (or with an "Accept: application/json" header to // find out the set of legacy interaction methods). // // The waitURL parameter holds a URL that can be long-polled // to acquire the discharge macaroon. func SetLegacyInteraction(e *Error, visitURL, waitURL string) { if e.Info == nil { e.Info = new(ErrorInfo) } e.Info.LegacyVisitURL = visitURL e.Info.LegacyWaitURL = waitURL } // WebBrowserInteractor handls web-browser-based // interaction-required errors by opening a web // browser to allow the user to prove their // credentials interactively. // // It implements the Interactor interface, so instances // can be used with Client.AddInteractor. type WebBrowserInteractor struct { // OpenWebBrowser is used to visit a page in // the user's web browser. If it's nil, the // OpenWebBrowser function will be used. OpenWebBrowser func(*url.URL) error } // Kind implements Interactor.Kind. func (WebBrowserInteractor) Kind() string { return WebBrowserInteractionKind } // Interact implements Interactor.Interact by opening a new web page. func (wi WebBrowserInteractor) Interact(ctx context.Context, client *Client, location string, irErr *Error) (*DischargeToken, error) { var p WebBrowserInteractionInfo if err := irErr.InteractionMethod(wi.Kind(), &p); err != nil { return nil, errgo.Mask(err, errgo.Is(ErrInteractionMethodNotFound)) } visitURL, err := relativeURL(location, p.VisitURL) if err != nil { return nil, errgo.Notef(err, "cannot make relative visit URL") } waitTokenURL, err := relativeURL(location, p.WaitTokenURL) if err != nil { return nil, errgo.Notef(err, "cannot make relative wait URL") } if err := wi.openWebBrowser(visitURL); err != nil { return nil, errgo.Mask(err) } return waitForToken(ctx, client, waitTokenURL) } func (wi WebBrowserInteractor) openWebBrowser(u *url.URL) error { open := wi.OpenWebBrowser if open == nil { open = OpenWebBrowser } if err := open(u); err != nil { return errgo.Mask(err) } return nil } // waitForToken returns a token from a the waitToken URL func waitForToken(ctx context.Context, client *Client, waitTokenURL *url.URL) (*DischargeToken, error) { // TODO integrate this with waitForMacaroon somehow? req, err := http.NewRequest("GET", waitTokenURL.String(), nil) if err != nil { return nil, errgo.Mask(err) } req = req.WithContext(ctx) httpResp, err := client.Client.Do(req) if err != nil { return nil, errgo.Notef(err, "cannot get %q", waitTokenURL) } defer httpResp.Body.Close() if httpResp.StatusCode != http.StatusOK { err := unmarshalError(httpResp) return nil, errgo.NoteMask(err, "cannot acquire discharge token", errgo.Any) } var resp WaitTokenResponse if err := httprequest.UnmarshalJSONResponse(httpResp, &resp); err != nil { return nil, errgo.Notef(err, "cannot unmarshal wait response") } tokenVal, err := maybeBase64Decode(resp.Token, resp.Token64) if err != nil { return nil, errgo.Notef(err, "bad discharge token") } // TODO check that kind and value are non-empty? return &DischargeToken{ Kind: resp.Kind, Value: tokenVal, }, nil } // LegacyInteract implements LegacyInteractor by opening a web browser page. func (wi WebBrowserInteractor) LegacyInteract(ctx context.Context, client *Client, location string, visitURL *url.URL) error { if err := wi.openWebBrowser(visitURL); err != nil { return errgo.Mask(err) } return nil } macaroon-bakery-3.0.2/httpbakery/checkers.go000066400000000000000000000110251464427415500210740ustar00rootroot00000000000000package httpbakery import ( "context" "net" "net/http" "gopkg.in/errgo.v1" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/checkers" ) type httpRequestKey struct{} // ContextWithRequest returns the context with information from the // given request attached as context. This is used by the httpbakery // checkers (see RegisterCheckers for details). func ContextWithRequest(ctx context.Context, req *http.Request) context.Context { return context.WithValue(ctx, httpRequestKey{}, req) } func requestFromContext(ctx context.Context) *http.Request { req, _ := ctx.Value(httpRequestKey{}).(*http.Request) return req } const ( // CondClientIPAddr holds the first party caveat condition // that checks a client's IP address. CondClientIPAddr = "client-ip-addr" // CondClientOrigin holds the first party caveat condition that // checks a client's origin header. CondClientOrigin = "origin" ) // CheckersNamespace holds the URI of the HTTP checkers schema. const CheckersNamespace = "http" var allCheckers = map[string]checkers.Func{ CondClientIPAddr: ipAddrCheck, CondClientOrigin: clientOriginCheck, } // RegisterCheckers registers all the HTTP checkers with the given checker. // Current checkers include: // // client-ip-addr // // The client-ip-addr caveat checks that the HTTP request has // the given remote IP address. // // origin // // The origin caveat checks that the HTTP Origin header has // the given value. func RegisterCheckers(c *checkers.Checker) { c.Namespace().Register(CheckersNamespace, "http") for cond, check := range allCheckers { c.Register(cond, CheckersNamespace, check) } } // NewChecker returns a new checker with the standard // and HTTP checkers registered in it. func NewChecker() *checkers.Checker { c := checkers.New(nil) RegisterCheckers(c) return c } // ipAddrCheck implements the IP client address checker // for an HTTP request. func ipAddrCheck(ctx context.Context, cond, args string) error { req := requestFromContext(ctx) if req == nil { return errgo.Newf("no IP address found in context") } ip := net.ParseIP(args) if ip == nil { return errgo.Newf("cannot parse IP address in caveat") } if req.RemoteAddr == "" { return errgo.Newf("client has no remote address") } reqIP, err := requestIPAddr(req) if err != nil { return errgo.Mask(err) } if !reqIP.Equal(ip) { return errgo.Newf("client IP address mismatch, got %s", reqIP) } return nil } // clientOriginCheck implements the Origin header checker // for an HTTP request. func clientOriginCheck(ctx context.Context, cond, args string) error { req := requestFromContext(ctx) if req == nil { return errgo.Newf("no origin found in context") } // Note that web browsers may not provide the origin header when it's // not a cross-site request with a GET method. There's nothing we // can do about that, so just allow all requests with an empty origin. if reqOrigin := req.Header.Get("Origin"); reqOrigin != "" && reqOrigin != args { return errgo.Newf("request has invalid Origin header; got %q", reqOrigin) } return nil } // SameClientIPAddrCaveat returns a caveat that will check that // the remote IP address is the same as that in the given HTTP request. func SameClientIPAddrCaveat(req *http.Request) checkers.Caveat { if req.RemoteAddr == "" { return checkers.ErrorCaveatf("client has no remote IP address") } ip, err := requestIPAddr(req) if err != nil { return checkers.ErrorCaveatf("%v", err) } return ClientIPAddrCaveat(ip) } // ClientIPAddrCaveat returns a caveat that will check whether the // client's IP address is as provided. func ClientIPAddrCaveat(addr net.IP) checkers.Caveat { if len(addr) != net.IPv4len && len(addr) != net.IPv6len { return checkers.ErrorCaveatf("bad IP address %d", []byte(addr)) } return httpCaveat(CondClientIPAddr, addr.String()) } // ClientOriginCaveat returns a caveat that will check whether the // client's Origin header in its HTTP request is as provided. func ClientOriginCaveat(origin string) checkers.Caveat { return httpCaveat(CondClientOrigin, origin) } func httpCaveat(cond, arg string) checkers.Caveat { return checkers.Caveat{ Condition: checkers.Condition(cond, arg), Namespace: CheckersNamespace, } } func requestIPAddr(req *http.Request) (net.IP, error) { reqHost, _, err := net.SplitHostPort(req.RemoteAddr) if err != nil { return nil, errgo.Newf("cannot parse host port in remote address: %v", err) } ip := net.ParseIP(reqHost) if ip == nil { return nil, errgo.Newf("invalid IP address in remote address %q", req.RemoteAddr) } return ip, nil } macaroon-bakery-3.0.2/httpbakery/checkers_test.go000066400000000000000000000131361464427415500221400ustar00rootroot00000000000000package httpbakery_test import ( "net" "net/http" "testing" qt "github.com/frankban/quicktest" "gopkg.in/errgo.v1" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/checkers" "github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery" ) type checkTest struct { caveat checkers.Caveat expectError string expectCause func(err error) bool } func caveatWithCondition(cond string) checkers.Caveat { return checkers.Caveat{ Condition: cond, } } var checkerTests = []struct { about string req *http.Request checks []checkTest }{{ about: "no host name declared", req: &http.Request{}, checks: []checkTest{{ caveat: httpbakery.ClientIPAddrCaveat(net.IP{0, 0, 0, 0}), expectError: `caveat "http:client-ip-addr 0.0.0.0" not satisfied: client has no remote address`, }, { caveat: httpbakery.ClientIPAddrCaveat(net.IP{127, 0, 0, 1}), expectError: `caveat "http:client-ip-addr 127.0.0.1" not satisfied: client has no remote address`, }, { caveat: caveatWithCondition("http:client-ip-addr badip"), expectError: `caveat "http:client-ip-addr badip" not satisfied: cannot parse IP address in caveat`, }}, }, { about: "IPv4 host name declared", req: &http.Request{ RemoteAddr: "127.0.0.1:1234", }, checks: []checkTest{{ caveat: httpbakery.ClientIPAddrCaveat(net.IP{127, 0, 0, 1}), }, { caveat: httpbakery.ClientIPAddrCaveat(net.IP{127, 0, 0, 1}.To16()), }, { caveat: caveatWithCondition("http:client-ip-addr ::ffff:7f00:1"), }, { caveat: httpbakery.ClientIPAddrCaveat(net.IP{127, 0, 0, 2}), expectError: `caveat "http:client-ip-addr 127.0.0.2" not satisfied: client IP address mismatch, got 127.0.0.1`, }, { caveat: httpbakery.ClientIPAddrCaveat(net.ParseIP("2001:4860:0:2001::68")), expectError: `caveat "http:client-ip-addr 2001:4860:0:2001::68" not satisfied: client IP address mismatch, got 127.0.0.1`, }}, }, { about: "IPv6 host name declared", req: &http.Request{ RemoteAddr: "[2001:4860:0:2001::68]:1234", }, checks: []checkTest{{ caveat: httpbakery.ClientIPAddrCaveat(net.ParseIP("2001:4860:0:2001::68")), }, { caveat: caveatWithCondition("http:client-ip-addr 2001:4860:0:2001:0::68"), }, { caveat: httpbakery.ClientIPAddrCaveat(net.ParseIP("2001:4860:0:2001::69")), expectError: `caveat "http:client-ip-addr 2001:4860:0:2001::69" not satisfied: client IP address mismatch, got 2001:4860:0:2001::68`, }, { caveat: httpbakery.ClientIPAddrCaveat(net.ParseIP("127.0.0.1")), expectError: `caveat "http:client-ip-addr 127.0.0.1" not satisfied: client IP address mismatch, got 2001:4860:0:2001::68`, }}, }, { about: "same client address, ipv4 request address", req: &http.Request{ RemoteAddr: "127.0.0.1:1324", }, checks: []checkTest{{ caveat: httpbakery.SameClientIPAddrCaveat(&http.Request{ RemoteAddr: "127.0.0.1:1234", }), }, { caveat: httpbakery.SameClientIPAddrCaveat(&http.Request{ RemoteAddr: "[::ffff:7f00:1]:1235", }), }, { caveat: httpbakery.SameClientIPAddrCaveat(&http.Request{ RemoteAddr: "127.0.0.2:1234", }), expectError: `caveat "http:client-ip-addr 127.0.0.2" not satisfied: client IP address mismatch, got 127.0.0.1`, }, { caveat: httpbakery.SameClientIPAddrCaveat(&http.Request{ RemoteAddr: "[::ffff:7f00:2]:1235", }), expectError: `caveat "http:client-ip-addr 127.0.0.2" not satisfied: client IP address mismatch, got 127.0.0.1`, }, { caveat: httpbakery.SameClientIPAddrCaveat(&http.Request{}), expectError: `caveat "error client has no remote IP address" not satisfied: bad caveat`, }, { caveat: httpbakery.SameClientIPAddrCaveat(&http.Request{ RemoteAddr: "bad", }), expectError: `caveat "error cannot parse host port in remote address: .*" not satisfied: bad caveat`, }, { caveat: httpbakery.SameClientIPAddrCaveat(&http.Request{ RemoteAddr: "bad:56", }), expectError: `caveat "error invalid IP address in remote address \\"bad:56\\"" not satisfied: bad caveat`, }}, }, { about: "same client address, ipv6 request address", req: &http.Request{ RemoteAddr: "[2001:4860:0:2001:0::68]:1235", }, checks: []checkTest{{ caveat: httpbakery.SameClientIPAddrCaveat(&http.Request{ RemoteAddr: "[2001:4860:0:2001:0::68]:1234", }), }, { caveat: httpbakery.SameClientIPAddrCaveat(&http.Request{ RemoteAddr: "127.0.0.2:1234", }), expectError: `caveat "http:client-ip-addr 127.0.0.2" not satisfied: client IP address mismatch, got 2001:4860:0:2001::68`, }}, }, { about: "request with no origin", req: &http.Request{}, checks: []checkTest{{ caveat: httpbakery.ClientOriginCaveat(""), }, { caveat: httpbakery.ClientOriginCaveat("somewhere"), }}, }, { about: "request with origin", req: &http.Request{ Header: http.Header{ "Origin": {"somewhere"}, }, }, checks: []checkTest{{ caveat: httpbakery.ClientOriginCaveat(""), expectError: `caveat "http:origin" not satisfied: request has invalid Origin header; got "somewhere"`, }, { caveat: httpbakery.ClientOriginCaveat("somewhere"), }}, }} func TestCheckers(t *testing.T) { c := qt.New(t) checker := httpbakery.NewChecker() for i, test := range checkerTests { c.Logf("test %d: %s", i, test.about) ctx := httpbakery.ContextWithRequest(testContext, test.req) for j, check := range test.checks { c.Logf("\tcheck %d", j) err := checker.CheckFirstPartyCaveat(ctx, checker.Namespace().ResolveCaveat(check.caveat).Condition) if check.expectError != "" { c.Assert(err, qt.ErrorMatches, check.expectError) if check.expectCause == nil { check.expectCause = errgo.Any } c.Assert(check.expectCause(errgo.Cause(err)), qt.Equals, true) } else { c.Assert(err, qt.IsNil) } } } } macaroon-bakery-3.0.2/httpbakery/client.go000066400000000000000000000601111464427415500205630ustar00rootroot00000000000000package httpbakery import ( "context" "encoding/base64" "encoding/json" "fmt" "net/http" "net/http/cookiejar" "net/url" "strings" "time" "golang.org/x/net/publicsuffix" "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" ) var unmarshalError = httprequest.ErrorUnmarshaler(&Error{}) // maxDischargeRetries holds the maximum number of times that an HTTP // request will be retried after a third party caveat has been successfully // discharged. const maxDischargeRetries = 3 // DischargeError represents the error when a third party discharge // is refused by a server. type DischargeError struct { // Reason holds the underlying remote error that caused the // discharge to fail. Reason *Error } func (e *DischargeError) Error() string { return fmt.Sprintf("third party refused discharge: %v", e.Reason) } // IsDischargeError reports whether err is a *DischargeError. func IsDischargeError(err error) bool { _, ok := err.(*DischargeError) return ok } // InteractionError wraps an error returned by a call to visitWebPage. type InteractionError struct { // Reason holds the actual error returned from visitWebPage. Reason error } func (e *InteractionError) Error() string { return fmt.Sprintf("cannot start interactive session: %v", e.Reason) } // IsInteractionError reports whether err is an *InteractionError. func IsInteractionError(err error) bool { _, ok := err.(*InteractionError) return ok } // NewHTTPClient returns an http.Client that ensures // that headers are sent to the server even when the // server redirects a GET request. The returned client // also contains an empty in-memory cookie jar. // // See https://github.com/golang/go/issues/4677 func NewHTTPClient() *http.Client { c := *http.DefaultClient c.CheckRedirect = func(req *http.Request, via []*http.Request) error { if len(via) >= 10 { return fmt.Errorf("too many redirects") } if len(via) == 0 { return nil } for attr, val := range via[0].Header { if attr == "Cookie" { // Cookies are added automatically anyway. continue } if _, ok := req.Header[attr]; !ok { req.Header[attr] = val } } return nil } jar, err := cookiejar.New(&cookiejar.Options{ PublicSuffixList: publicsuffix.List, }) if err != nil { panic(err) } c.Jar = jar return &c } // Client holds the context for making HTTP requests // that automatically acquire and discharge macaroons. type Client struct { // Client holds the HTTP client to use. It should have a cookie // jar configured, and when redirecting it should preserve the // headers (see NewHTTPClient). *http.Client // InteractionMethods holds a slice of supported interaction // methods, with preferred methods earlier in the slice. // On receiving an interaction-required error when discharging, // the Kind method of each Interactor in turn will be called // and, if the error indicates that the interaction kind is supported, // the Interact method will be called to complete the discharge. InteractionMethods []Interactor // Key holds the client's key. If set, the client will try to // discharge third party caveats with the special location // "local" by using this key. See bakery.DischargeAllWithKey and // bakery.LocalThirdPartyCaveat for more information Key *bakery.KeyPair // Logger is used to log information about client activities. // If it is nil, bakery.DefaultLogger("httpbakery") will be used. Logger bakery.Logger } // An Interactor represents a way of persuading a discharger // that it should grant a discharge macaroon. type Interactor interface { // Kind returns the interaction method name. This corresponds to the // key in the Error.InteractionMethods type. Kind() string // Interact performs the interaction, and returns a token that can be // used to acquire the discharge macaroon. The location provides // the third party caveat location to make it possible to use // relative URLs. // // If the given interaction isn't supported by the client for // the given location, it may return an error with an // ErrInteractionMethodNotFound cause which will cause the // interactor to be ignored that time. Interact(ctx context.Context, client *Client, location string, interactionRequiredErr *Error) (*DischargeToken, error) } // DischargeToken holds a token that is intended // to persuade a discharger to discharge a third // party caveat. type DischargeToken struct { // Kind holds the kind of the token. By convention this // matches the name of the interaction method used to // obtain the token, but that's not required. Kind string `json:"kind"` // Value holds the value of the token. Value []byte `json:"value"` } // LegacyInteractor may optionally be implemented by Interactor // implementations that implement the legacy interaction-required // error protocols. type LegacyInteractor interface { // LegacyInteract implements the "visit" half of a legacy discharge // interaction. The "wait" half will be implemented by httpbakery. // The location is the location specified by the third party // caveat. LegacyInteract(ctx context.Context, client *Client, location string, visitURL *url.URL) error } // NewClient returns a new Client containing an HTTP client // created with NewHTTPClient and leaves all other fields zero. func NewClient() *Client { return &Client{ Client: NewHTTPClient(), } } // AddInteractor is a convenience method that appends the given // interactor to c.InteractionMethods. // For example, to enable web-browser interaction on // a client c, do: // // c.AddInteractor(httpbakery.WebBrowserWindowInteractor) func (c *Client) AddInteractor(i Interactor) { c.InteractionMethods = append(c.InteractionMethods, i) } // DischargeAll attempts to acquire discharge macaroons for all the // third party caveats in m, and returns a slice containing all // of them bound to m. // // If the discharge fails because a third party refuses to discharge a // caveat, the returned error will have a cause of type *DischargeError. // If the discharge fails because visitWebPage returns an error, // the returned error will have a cause of *InteractionError. // // The returned macaroon slice will not be stored in the client // cookie jar (see SetCookie if you need to do that). func (c *Client) DischargeAll(ctx context.Context, m *bakery.Macaroon) (macaroon.Slice, error) { return bakery.DischargeAllWithKey(ctx, m, c.AcquireDischarge, c.Key) } // DischargeAllUnbound is like DischargeAll except that it does not // bind the resulting macaroons. func (c *Client) DischargeAllUnbound(ctx context.Context, ms bakery.Slice) (bakery.Slice, error) { return ms.DischargeAll(ctx, c.AcquireDischarge, c.Key) } // Do is like DoWithContext, except the context is automatically derived. // If using go version 1.7 or later the context will be taken from the // given request, otherwise context.Background() will be used. func (c *Client) Do(req *http.Request) (*http.Response, error) { return c.do(contextFromRequest(req), req, nil) } // DoWithContext sends the given HTTP request and returns its response. // If the request fails with a discharge-required error, any required // discharge macaroons will be acquired, and the request will be repeated // with those attached. // // If the required discharges were refused by a third party, an error // with a *DischargeError cause will be returned. // // If interaction is required by the user, the client's InteractionMethods // will be used to perform interaction. An error // with a *InteractionError cause will be returned if this interaction // fails. See WebBrowserWindowInteractor for a possible implementation of // an Interactor for an interaction method. // // DoWithContext may add headers to req.Header. func (c *Client) DoWithContext(ctx context.Context, req *http.Request) (*http.Response, error) { return c.do(ctx, req, nil) } // DoWithCustomError is like Do except it allows a client // to specify a custom error function, getError, which is called on the // HTTP response and may return a non-nil error if the response holds an // error. If the cause of the returned error is a *Error value and its // code is ErrDischargeRequired, the macaroon in its Info field will be // discharged and the request will be repeated with the discharged // macaroon. If getError returns nil, it should leave the response body // unchanged. // // If getError is nil, DefaultGetError will be used. // // This method can be useful when dealing with APIs that // return their errors in a format incompatible with Error, but the // need for it should be avoided when creating new APIs, // as it makes the endpoints less amenable to generic tools. func (c *Client) DoWithCustomError(req *http.Request, getError func(resp *http.Response) error) (*http.Response, error) { return c.do(contextFromRequest(req), req, getError) } func (c *Client) do(ctx context.Context, req *http.Request, getError func(resp *http.Response) error) (*http.Response, error) { c.logDebugf(ctx, "client do %s %s {", req.Method, req.URL) resp, err := c.do1(ctx, req, getError) c.logDebugf(ctx, "} -> error %#v", err) return resp, err } func (c *Client) do1(ctx context.Context, req *http.Request, getError func(resp *http.Response) error) (*http.Response, error) { if getError == nil { getError = DefaultGetError } if c.Client.Jar == nil { return nil, errgo.New("no cookie jar supplied in HTTP client") } rreq, ok := newRetryableRequest(c.Client, req) if !ok { return nil, fmt.Errorf("request body is not seekable") } defer rreq.close() req.Header.Set(BakeryProtocolHeader, fmt.Sprint(bakery.LatestVersion)) // Make several attempts to do the request, because we might have // to get through several layers of security. We only retry if // we get a DischargeRequiredError and succeed in discharging // the macaroon in it. retry := 0 for { resp, err := c.do2(ctx, rreq, getError) if err == nil || !isDischargeRequiredError(err) { return resp, errgo.Mask(err, errgo.Any) } if retry++; retry > maxDischargeRetries { return nil, errgo.NoteMask(err, fmt.Sprintf("too many (%d) discharge requests", retry-1), errgo.Any) } if err1 := c.HandleError(ctx, req.URL, err); err1 != nil { return nil, errgo.Mask(err1, errgo.Any) } c.logDebugf(ctx, "discharge succeeded; retry %d", retry) } } func (c *Client) do2(ctx context.Context, rreq *retryableRequest, getError func(resp *http.Response) error) (*http.Response, error) { httpResp, err := rreq.do(ctx) if err != nil { return nil, errgo.Mask(err, errgo.Any) } err = getError(httpResp) if err == nil { c.logInfof(ctx, "HTTP response OK (status %v)", httpResp.Status) return httpResp, nil } httpResp.Body.Close() return nil, errgo.Mask(err, errgo.Any) } // HandleError tries to resolve the given error, which should be a // response to the given URL, by discharging any macaroon contained in // it. That is, if the error cause is an *Error and its code is // ErrDischargeRequired, then it will try to discharge // err.Info.Macaroon. If the discharge succeeds, the discharged macaroon // will be saved to the client's cookie jar and ResolveError will return // nil. // // For any other kind of error, the original error will be returned. func (c *Client) HandleError(ctx context.Context, reqURL *url.URL, err error) error { respErr, ok := errgo.Cause(err).(*Error) if !ok { return err } if respErr.Code != ErrDischargeRequired { return respErr } if respErr.Info == nil || respErr.Info.Macaroon == nil { return errgo.New("no macaroon found in discharge-required response") } mac := respErr.Info.Macaroon macaroons, err := bakery.DischargeAllWithKey(ctx, mac, c.AcquireDischarge, c.Key) if err != nil { return errgo.Mask(err, errgo.Any) } var cookiePath string if path := respErr.Info.MacaroonPath; path != "" { relURL, err := parseURLPath(path) if err != nil { c.logInfof(ctx, "ignoring invalid path in discharge-required response: %v", err) } else { cookiePath = reqURL.ResolveReference(relURL).Path } } // TODO use a namespace taken from the error response. cookie, err := NewCookie(nil, macaroons) if err != nil { return errgo.Notef(err, "cannot make cookie") } cookie.Path = cookiePath if name := respErr.Info.CookieNameSuffix; name != "" { cookie.Name = "macaroon-" + name } c.Jar.SetCookies(reqURL, []*http.Cookie{cookie}) return nil } // DefaultGetError is the default error unmarshaler used by Client.Do. func DefaultGetError(httpResp *http.Response) error { if httpResp.StatusCode != http.StatusProxyAuthRequired && httpResp.StatusCode != http.StatusUnauthorized { return nil } // Check for the new protocol discharge error. if httpResp.StatusCode == http.StatusUnauthorized && httpResp.Header.Get("WWW-Authenticate") != "Macaroon" { return nil } if httpResp.Header.Get("Content-Type") != "application/json" { return nil } var resp Error if err := json.NewDecoder(httpResp.Body).Decode(&resp); err != nil { return fmt.Errorf("cannot unmarshal error response: %v", err) } return &resp } func parseURLPath(path string) (*url.URL, error) { u, err := url.Parse(path) if err != nil { return nil, errgo.Mask(err) } if u.Scheme != "" || u.Opaque != "" || u.User != nil || u.Host != "" || u.RawQuery != "" || u.Fragment != "" { return nil, errgo.Newf("URL path %q is not clean", path) } return u, nil } // PermanentExpiryDuration holds the length of time a cookie // holding a macaroon with no time-before caveat will be // stored. const PermanentExpiryDuration = 100 * 365 * 24 * time.Hour // NewCookie takes a slice of macaroons and returns them // encoded as a cookie. The slice should contain a single primary // macaroon in its first element, and any discharges after that. // // The given namespace specifies the first party caveat namespace, // used for deriving the expiry time of the cookie. func NewCookie(ns *checkers.Namespace, ms macaroon.Slice) (*http.Cookie, error) { if len(ms) == 0 { return nil, errgo.New("no macaroons in cookie") } // TODO(rog) marshal cookie as binary if version allows. data, err := json.Marshal(ms) if err != nil { return nil, errgo.Notef(err, "cannot marshal macaroons") } cookie := &http.Cookie{ Name: fmt.Sprintf("macaroon-%x", ms[0].Signature()), Value: base64.StdEncoding.EncodeToString(data), } expires, found := checkers.MacaroonsExpiryTime(ns, ms) if !found { // The macaroon doesn't expire - use a very long expiry // time for the cookie. expires = time.Now().Add(PermanentExpiryDuration) } else if expires.Sub(time.Now()) < time.Minute { // The macaroon might have expired already, or it's // got a short duration, so treat it as a session cookie // by setting Expires to the zero time. expires = time.Time{} } cookie.Expires = expires // TODO(rog) other fields. return cookie, nil } // SetCookie sets a cookie for the given URL on the given cookie jar // that will holds the given macaroon slice. The macaroon slice should // contain a single primary macaroon in its first element, and any // discharges after that. // // The given namespace specifies the first party caveat namespace, // used for deriving the expiry time of the cookie. func SetCookie(jar http.CookieJar, url *url.URL, ns *checkers.Namespace, ms macaroon.Slice) error { cookie, err := NewCookie(ns, ms) if err != nil { return errgo.Mask(err) } jar.SetCookies(url, []*http.Cookie{cookie}) return nil } // MacaroonsForURL returns any macaroons associated with the // given URL in the given cookie jar. func MacaroonsForURL(jar http.CookieJar, u *url.URL) []macaroon.Slice { return cookiesToMacaroons(jar.Cookies(u)) } func appendURLElem(u, elem string) string { if strings.HasSuffix(u, "/") { return u + elem } return u + "/" + elem } // AcquireDischarge acquires a discharge macaroon from the caveat location as an HTTP URL. // It fits the getDischarge argument type required by bakery.DischargeAll. func (c *Client) AcquireDischarge(ctx context.Context, cav macaroon.Caveat, payload []byte) (*bakery.Macaroon, error) { m, err := c.acquireDischarge(ctx, cav, payload, nil) if err == nil { return m, nil } cause, ok := errgo.Cause(err).(*Error) if !ok { return nil, errgo.NoteMask(err, "cannot acquire discharge", IsInteractionError) } if cause.Code != ErrInteractionRequired { return nil, &DischargeError{ Reason: cause, } } if cause.Info == nil { return nil, errgo.Notef(err, "interaction-required response with no info") } // Make sure the location has a trailing slash so that // the relative URL calculations work correctly even when // cav.Location doesn't have a trailing slash. loc := appendURLElem(cav.Location, "") token, m, err := c.interact(ctx, loc, cause, payload) if err != nil { return nil, errgo.Mask(err, IsDischargeError, IsInteractionError) } if m != nil { // We've acquired the macaroon directly via legacy interaction. return m, nil } // Try to acquire the discharge again, but this time with // the token acquired by the interaction method. m, err = c.acquireDischarge(ctx, cav, payload, token) if err != nil { return nil, errgo.Mask(err, IsDischargeError, IsInteractionError) } return m, nil } // acquireDischarge is like AcquireDischarge except that it also // takes a token acquired from an interaction method. func (c *Client) acquireDischarge( ctx context.Context, cav macaroon.Caveat, payload []byte, token *DischargeToken, ) (*bakery.Macaroon, error) { dclient := newDischargeClient(cav.Location, c) var req dischargeRequest req.Id, req.Id64 = maybeBase64Encode(cav.Id) if token != nil { req.Token, req.Token64 = maybeBase64Encode(token.Value) req.TokenKind = token.Kind } req.Caveat = base64.RawURLEncoding.EncodeToString(payload) resp, err := dclient.Discharge(ctx, &req) if err == nil { return resp.Macaroon, nil } return nil, errgo.Mask(err, errgo.Any) } // interact gathers a macaroon by directing the user to interact with a // web page. The irErr argument holds the interaction-required // error response. func (c *Client) interact(ctx context.Context, location string, irErr *Error, payload []byte) (*DischargeToken, *bakery.Macaroon, error) { if len(c.InteractionMethods) == 0 { return nil, nil, &InteractionError{ Reason: errgo.New("interaction required but not possible"), } } if irErr.Info.InteractionMethods == nil && irErr.Info.LegacyVisitURL != "" { // It's an old-style error; deal with it differently. m, err := c.legacyInteract(ctx, location, irErr) if err != nil { return nil, nil, errgo.Mask(err, IsDischargeError, IsInteractionError) } return nil, m, nil } for _, interactor := range c.InteractionMethods { c.logDebugf(ctx, "checking interaction method %q", interactor.Kind()) if _, ok := irErr.Info.InteractionMethods[interactor.Kind()]; ok { c.logDebugf(ctx, "found possible interaction method %q", interactor.Kind()) token, err := interactor.Interact(ctx, c, location, irErr) if err != nil { if errgo.Cause(err) == ErrInteractionMethodNotFound { continue } return nil, nil, errgo.Mask(err, IsDischargeError, IsInteractionError) } if token == nil { return nil, nil, errgo.New("interaction method returned an empty token") } return token, nil, nil } else { c.logDebugf(ctx, "interaction method %q not found in %#v", interactor.Kind(), irErr.Info.InteractionMethods) } } return nil, nil, &InteractionError{ Reason: errgo.Newf("no supported interaction method"), } } func (c *Client) legacyInteract(ctx context.Context, location string, irErr *Error) (*bakery.Macaroon, error) { visitURL, err := relativeURL(location, irErr.Info.LegacyVisitURL) if err != nil { return nil, errgo.Mask(err) } waitURL, err := relativeURL(location, irErr.Info.LegacyWaitURL) if err != nil { return nil, errgo.Mask(err) } methodURLs := map[string]*url.URL{ "interactive": visitURL, } if len(c.InteractionMethods) > 1 || c.InteractionMethods[0].Kind() != WebBrowserInteractionKind { // We have several possible methods or we only support a non-window // method, so we need to fetch the possible methods supported by the discharger. methodURLs = legacyGetInteractionMethods(ctx, c.logger(), c, visitURL) } for _, interactor := range c.InteractionMethods { kind := interactor.Kind() if kind == WebBrowserInteractionKind { // This is the old name for browser-window interaction. kind = "interactive" } interactor, ok := interactor.(LegacyInteractor) if !ok { // Legacy interaction mode isn't supported. continue } visitURL, ok := methodURLs[kind] if !ok { continue } visitURL, err := relativeURL(location, visitURL.String()) if err != nil { return nil, errgo.Mask(err) } if err := interactor.LegacyInteract(ctx, c, location, visitURL); err != nil { return nil, &InteractionError{ Reason: errgo.Mask(err, errgo.Any), } } return waitForMacaroon(ctx, c, waitURL) } return nil, &InteractionError{ Reason: errgo.Newf("no methods supported"), } } func (c *Client) logDebugf(ctx context.Context, f string, a ...interface{}) { c.logger().Debugf(ctx, f, a...) } func (c *Client) logInfof(ctx context.Context, f string, a ...interface{}) { c.logger().Infof(ctx, f, a...) } func (c *Client) logger() bakery.Logger { if c.Logger != nil { return c.Logger } return bakery.DefaultLogger("httpbakery") } // waitForMacaroon returns a macaroon from a legacy wait endpoint. func waitForMacaroon(ctx context.Context, client *Client, waitURL *url.URL) (*bakery.Macaroon, error) { req, err := http.NewRequest("GET", waitURL.String(), nil) if err != nil { return nil, errgo.Mask(err) } req = req.WithContext(ctx) httpResp, err := client.Client.Do(req) if err != nil { return nil, errgo.Notef(err, "cannot get %q", waitURL) } defer httpResp.Body.Close() if httpResp.StatusCode != http.StatusOK { err := unmarshalError(httpResp) if err1, ok := err.(*Error); ok { err = &DischargeError{ Reason: err1, } } return nil, errgo.NoteMask(err, "failed to acquire macaroon after waiting", errgo.Any) } var resp WaitResponse if err := httprequest.UnmarshalJSONResponse(httpResp, &resp); err != nil { return nil, errgo.Notef(err, "cannot unmarshal wait response") } return resp.Macaroon, nil } // relativeURL returns newPath relative to an original URL. func relativeURL(base, new string) (*url.URL, error) { if new == "" { return nil, errgo.Newf("empty URL") } baseURL, err := url.Parse(base) if err != nil { return nil, errgo.Notef(err, "cannot parse URL") } newURL, err := url.Parse(new) if err != nil { return nil, errgo.Notef(err, "cannot parse URL") } return baseURL.ResolveReference(newURL), nil } // TODO(rog) move a lot of the code below into server.go, as it's // much more about server side than client side. // MacaroonsHeader is the key of the HTTP header that can be used to provide a // macaroon for request authorization. const MacaroonsHeader = "Macaroons" // RequestMacaroons returns any collections of macaroons from the header and // cookies found in the request. By convention, each slice will contain a // primary macaroon followed by its discharges. func RequestMacaroons(req *http.Request) []macaroon.Slice { mss := cookiesToMacaroons(req.Cookies()) for _, h := range req.Header[MacaroonsHeader] { ms, err := decodeMacaroonSlice(h) if err != nil { // Ignore invalid macaroons. continue } mss = append(mss, ms) } return mss } // cookiesToMacaroons returns a slice of any macaroons found // in the given slice of cookies. func cookiesToMacaroons(cookies []*http.Cookie) []macaroon.Slice { var mss []macaroon.Slice for _, cookie := range cookies { if !strings.HasPrefix(cookie.Name, "macaroon-") { continue } ms, err := decodeMacaroonSlice(cookie.Value) if err != nil { // Ignore invalid macaroons. continue } mss = append(mss, ms) } return mss } // decodeMacaroonSlice decodes a base64-JSON-encoded slice of macaroons from // the given string. func decodeMacaroonSlice(value string) (macaroon.Slice, error) { data, err := macaroon.Base64Decode([]byte(value)) if err != nil { return nil, errgo.NoteMask(err, "cannot base64-decode macaroons") } // TODO(rog) accept binary encoded macaroon cookies. var ms macaroon.Slice if err := json.Unmarshal(data, &ms); err != nil { return nil, errgo.NoteMask(err, "cannot unmarshal macaroons") } return ms, nil } macaroon-bakery-3.0.2/httpbakery/client_test.go000066400000000000000000001314151464427415500216300ustar00rootroot00000000000000package httpbakery_test import ( "bytes" "context" "encoding/base64" "encoding/json" "fmt" "io" "io/ioutil" "net/http" "net/http/cookiejar" "net/http/httptest" "net/url" "sort" "strings" "testing" "time" qt "github.com/frankban/quicktest" "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" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakerytest" "github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery" ) var ( testOp = bakery.Op{"test", "test"} testContext = context.Background() ) // TestSingleServiceFirstParty creates a single service // with a macaroon with one first party caveat. // It creates a request with this macaroon and checks that the service // can verify this macaroon as valid. func TestSingleServiceFirstParty(t *testing.T) { c := qt.New(t) // Create a target service. b := newBakery("loc", nil, nil) // No discharge required, so pass "unknown" for the third party // caveat discharger location so we know that we don't try // to discharge the location. ts := httptest.NewServer(serverHandler(serverHandlerParams{ bakery: b, authLocation: "unknown", })) defer ts.Close() // Mint a macaroon for the target service. serverMacaroon, err := b.Oven.NewMacaroon(testContext, bakery.LatestVersion, nil, testOp) c.Assert(err, qt.IsNil) c.Assert(serverMacaroon.M().Location(), qt.Equals, "loc") err = b.Oven.AddCaveat(testContext, serverMacaroon, isSomethingCaveat()) c.Assert(err, qt.IsNil) // Create a client request. req, err := http.NewRequest("GET", ts.URL, nil) c.Assert(err, qt.IsNil) client := clientRequestWithCookies(c, ts.URL, macaroon.Slice{serverMacaroon.M()}) // Somehow the client has accquired the macaroon. Add it to the cookiejar in our request. // Make the request to the server. resp, err := client.Do(req) c.Assert(err, qt.IsNil) defer resp.Body.Close() assertResponse(c, resp, "done") } func TestSingleServiceFirstPartyWithHeader(t *testing.T) { c := qt.New(t) // Create a target service. b := newBakery("loc", nil, nil) // No discharge required, so pass "unknown" for the third party // caveat discharger location so we know that we don't try // to discharge the location. ts := httptest.NewServer(serverHandler(serverHandlerParams{ bakery: b, authLocation: "unknown", })) defer ts.Close() // Mint a macaroon for the target service. serverMacaroon, err := b.Oven.NewMacaroon(testContext, bakery.LatestVersion, nil, testOp) c.Assert(err, qt.IsNil) c.Assert(serverMacaroon.M().Location(), qt.Equals, "loc") err = b.Oven.AddCaveat(testContext, serverMacaroon, isSomethingCaveat()) c.Assert(err, qt.IsNil) // Serialize the macaroon slice. data, err := json.Marshal(macaroon.Slice{serverMacaroon.M()}) c.Assert(err, qt.IsNil) value := base64.StdEncoding.EncodeToString(data) // Create a client request. req, err := http.NewRequest("GET", ts.URL, nil) c.Assert(err, qt.IsNil) req.Header.Set(httpbakery.MacaroonsHeader, value) client := httpbakery.NewHTTPClient() // Make the request to the server. resp, err := client.Do(req) c.Assert(err, qt.IsNil) defer resp.Body.Close() assertResponse(c, resp, "done") } func TestRepeatedRequestWithBody(t *testing.T) { c := qt.New(t) d := bakerytest.NewDischarger(nil) defer d.Close() // Create a target service. b := newBakery("loc", d, nil) ts := httptest.NewServer(serverHandler(serverHandlerParams{ bakery: b, authLocation: d.Location(), alwaysReadBody: true, })) defer ts.Close() // Try with no authorization, to make sure that httpbakery.Do // really will retry the request. bodyText := "postbody" bodyReader := &readCounter{ReadSeeker: strings.NewReader(bodyText)} req, err := http.NewRequest("POST", ts.URL, bodyReader) c.Assert(err, qt.IsNil) resp, err := httpbakery.NewClient().Do(req) c.Assert(err, qt.IsNil) defer resp.Body.Close() assertResponse(c, resp, "done postbody") // Sanity check that the body really was read twice and hence // that we are checking the logic we intend to check. c.Assert(bodyReader.byteCount, qt.Equals, len(bodyText)*2) } func TestWithLargeBody(t *testing.T) { c := qt.New(t) // This test is designed to fail when run with the race // checker enabled and when go issue #12796 // is not fixed. d := bakerytest.NewDischarger(nil) defer d.Close() // Create a target service. b := newBakery("loc", d, nil) ts := httptest.NewServer(serverHandler(serverHandlerParams{ bakery: b, authLocation: d.Location(), })) defer ts.Close() // Create a client request. req, err := http.NewRequest("POST", ts.URL+"/no-body", &largeReader{total: 3 * 1024 * 1024}) c.Assert(err, qt.IsNil) resp, err := httpbakery.NewClient().Do(req) c.Assert(err, qt.IsNil) resp.Body.Close() c.Assert(resp.StatusCode, qt.Equals, http.StatusOK) } // largeReader implements a reader that produces up to total bytes // in 1 byte reads. type largeReader struct { total int n int } func (r *largeReader) Read(buf []byte) (int, error) { if r.n >= r.total { return 0, io.EOF } r.n++ return copy(buf, []byte("a")), nil } func (r *largeReader) Seek(offset int64, whence int) (int64, error) { if offset != 0 || whence != 0 { panic("unexpected seek") } r.n = 0 return 0, nil } func (r *largeReader) Close() error { // By setting n to zero, we ensure that if there's // a concurrent read, it will also read from n // and so the race detector should pick up the // problem. r.n = 0 return nil } func TestDischargeServerWithBinaryCaveatId(t *testing.T) { c := qt.New(t) assertDischargeServerDischargesConditionForVersion(c, "\xff\x00\x89", bakery.Version2) } func TestDischargeServerWithStringCaveatId(t *testing.T) { c := qt.New(t) assertDischargeServerDischargesConditionForVersion(c, "foo", bakery.Version1) } func assertDischargeServerDischargesConditionForVersion(c *qt.C, cond string, version bakery.Version) { called := 0 checker := func(ctx context.Context, p httpbakery.ThirdPartyCaveatCheckerParams) ([]checkers.Caveat, error) { called++ c.Check(string(p.Caveat.Condition), qt.Equals, cond) return nil, nil } discharger := bakerytest.NewDischarger(nil) discharger.CheckerP = httpbakery.ThirdPartyCaveatCheckerPFunc(checker) bKey, err := bakery.GenerateKey() c.Assert(err, qt.IsNil) m, err := bakery.NewMacaroon([]byte("root key"), []byte("id"), "location", version, nil) c.Assert(err, qt.IsNil) err = m.AddCaveat(context.TODO(), checkers.Caveat{ Location: discharger.Location(), Condition: cond, }, bKey, discharger) c.Assert(err, qt.IsNil) client := httpbakery.NewClient() ms, err := client.DischargeAll(context.TODO(), m) c.Assert(err, qt.IsNil) c.Check(ms, qt.HasLen, 2) c.Check(called, qt.Equals, 1) } func TestDoClosesBody(t *testing.T) { c := qt.New(t) cn := closeNotifier{ closed: make(chan struct{}), } req, err := http.NewRequest("GET", "http://0.1.2.3/", cn) c.Assert(err, qt.IsNil) _, err = httpbakery.NewClient().Do(req) c.Assert(err, qt.Not(qt.IsNil)) select { case <-cn.closed: case <-time.After(5 * time.Second): c.Fatalf("timed out waiting for request body to be closed") } } func TestWithNonSeekableBody(t *testing.T) { c := qt.New(t) r := bytes.NewBufferString("hello") req, err := http.NewRequest("GET", "http://0.1.2.3/", r) c.Assert(err, qt.IsNil) _, err = httpbakery.NewClient().Do(req) c.Assert(err, qt.ErrorMatches, `request body is not seekable`) } func TestWithNonSeekableCloserBody(t *testing.T) { c := qt.New(t) req, err := http.NewRequest("GET", "http://0.1.2.3/", readCloser{}) c.Assert(err, qt.IsNil) _, err = httpbakery.NewClient().Do(req) c.Assert(err, qt.ErrorMatches, `request body is not seekable`) } // Regression test for https://github.com/go-macaroon-bakery/macaroon-bakery/issues/276 // Test that client.Do(req) works when req.Body implements WriterTo func TestWithWriterToBody(t *testing.T) { c := qt.New(t) // Here we're just testing the newRetryableRequest logic // We don't care about the request or response srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { _, _ = w.Write([]byte("hello")) })) req, err := http.NewRequest("GET", srv.URL, bytes.NewReader([]byte{0, 1, 2, 3})) c.Assert(err, qt.IsNil) // req.Body = io.NopCloser(bytes.Reader) // In Go 1.19+, this has type io.nopCloserWriterTo, not io.nopCloser _, err = httpbakery.NewClient().Do(req) c.Assert(err, qt.IsNil) } type readCloser struct { } func (r readCloser) Read(buf []byte) (int, error) { return 0, io.EOF } func (r readCloser) Close() error { return nil } type closeNotifier struct { closed chan struct{} } func (r closeNotifier) Read(buf []byte) (int, error) { return 0, io.EOF } func (r closeNotifier) Seek(offset int64, whence int) (int64, error) { return 0, nil } func (r closeNotifier) Close() error { close(r.closed) return nil } func TestDischargeServerWithMacaraqOnDischarge(t *testing.T) { c := qt.New(t) locator := bakery.NewThirdPartyStore() var called [3]int // create the services from leaf discharger to primary // service so that each one can know the location // to discharge at. db1 := newBakery("loc", locator, nil) key2, h2 := newHTTPDischarger(db1, httpbakery.ThirdPartyCaveatCheckerPFunc(func(ctx context.Context, p httpbakery.ThirdPartyCaveatCheckerParams) ([]checkers.Caveat, error) { called[2]++ if string(p.Caveat.Condition) != "is-ok" { return nil, fmt.Errorf("unrecognized caveat at srv2") } return nil, nil })) srv2 := httptest.NewServer(h2) defer srv2.Close() locator.AddInfo(srv2.URL, bakery.ThirdPartyInfo{ PublicKey: key2, Version: bakery.LatestVersion, }) db2 := newBakery("loc", locator, nil) key1, h1 := newHTTPDischarger(db2, httpbakery.ThirdPartyCaveatCheckerPFunc(func(ctx context.Context, p httpbakery.ThirdPartyCaveatCheckerParams) ([]checkers.Caveat, error) { called[1]++ if _, err := db2.Checker.Auth(httpbakery.RequestMacaroons(p.Request)...).Allow(testContext, testOp); err != nil { c.Logf("returning discharge required error") return nil, newDischargeRequiredError(serverHandlerParams{ bakery: db2, authLocation: srv2.URL, }, err, p.Request) } if string(p.Caveat.Condition) != "is-ok" { return nil, fmt.Errorf("unrecognized caveat at srv1") } return nil, nil })) srv1 := httptest.NewServer(h1) defer srv1.Close() locator.AddInfo(srv1.URL, bakery.ThirdPartyInfo{ PublicKey: key1, Version: bakery.LatestVersion, }) b0 := newBakery("loc", locator, nil) srv0 := httptest.NewServer(serverHandler(serverHandlerParams{ bakery: b0, authLocation: srv1.URL, })) defer srv0.Close() // Make a client request. client := httpbakery.NewClient() req, err := http.NewRequest("GET", srv0.URL, nil) c.Assert(err, qt.IsNil) resp, err := client.Do(req) c.Assert(err, qt.IsNil) defer resp.Body.Close() assertResponse(c, resp, "done") c.Assert(called, qt.DeepEquals, [3]int{0, 2, 1}) } func TestTwoDischargesRequired(t *testing.T) { c := qt.New(t) // Sometimes the first discharge won't be enough and we'll // need to discharge another one to get through another // layer of security. dischargeCount := 0 checker := func(ctx context.Context, p httpbakery.ThirdPartyCaveatCheckerParams) ([]checkers.Caveat, error) { c.Check(string(p.Caveat.Condition), qt.Equals, "is-ok") dischargeCount++ return nil, nil } discharger := bakerytest.NewDischarger(nil) discharger.CheckerP = httpbakery.ThirdPartyCaveatCheckerPFunc(checker) srv := serverRequiringMultipleDischarges(httpbakery.MaxDischargeRetries, discharger) defer srv.Close() // Create a client request. req, err := http.NewRequest("GET", srv.URL, nil) c.Assert(err, qt.IsNil) resp, err := httpbakery.NewClient().Do(req) c.Assert(err, qt.IsNil) defer resp.Body.Close() c.Assert(resp.StatusCode, qt.Equals, http.StatusOK) data, err := ioutil.ReadAll(resp.Body) c.Assert(err, qt.IsNil) c.Assert(string(data), qt.Equals, "ok") c.Assert(dischargeCount, qt.Equals, httpbakery.MaxDischargeRetries) } func TestTooManyDischargesRequired(t *testing.T) { c := qt.New(t) checker := func(context.Context, httpbakery.ThirdPartyCaveatCheckerParams) ([]checkers.Caveat, error) { return nil, nil } discharger := bakerytest.NewDischarger(nil) discharger.CheckerP = httpbakery.ThirdPartyCaveatCheckerPFunc(checker) srv := serverRequiringMultipleDischarges(httpbakery.MaxDischargeRetries+1, discharger) defer srv.Close() // Create a client request. req, err := http.NewRequest("GET", srv.URL, nil) c.Assert(err, qt.IsNil) _, err = httpbakery.NewClient().Do(req) c.Assert(err, qt.ErrorMatches, `too many \(3\) discharge requests: foo`) } // multiDischargeServer returns a server that will require multiple // discharges when accessing its endpoints. The parameter // holds the total number of discharges that will be required. func serverRequiringMultipleDischarges(n int, discharger *bakerytest.Discharger) *httptest.Server { b := newBakery("loc", discharger, nil) return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { if hasDuplicateCookies(req) { panic(errgo.Newf("duplicate cookie names in request; cookies %s", req.Header["Cookie"])) } if _, err := b.Checker.Auth(httpbakery.RequestMacaroons(req)...).Allow(context.TODO(), testOp); err == nil { w.Write([]byte("ok")) return } caveats := []checkers.Caveat{{ Location: discharger.Location(), Condition: "is-ok", }} if n--; n > 0 { // We've got more attempts to go, so add a first party caveat that // will cause the macaroon to fail verification and so trigger // another discharge-required error. caveats = append(caveats, checkers.Caveat{ Condition: fmt.Sprintf("error %d attempts left", n), }) } m, err := b.Oven.NewMacaroon(context.TODO(), bakery.LatestVersion, caveats, testOp) if err != nil { panic(fmt.Errorf("cannot make new macaroon: %v", err)) } err = httpbakery.NewDischargeRequiredError(httpbakery.DischargeRequiredErrorParams{ OriginalError: errgo.New("foo"), Macaroon: m, CookieNameSuffix: fmt.Sprintf("auth%d", n), }) httpbakery.WriteError(testContext, w, err) })) } func hasDuplicateCookies(req *http.Request) bool { names := make(map[string]bool) for _, cookie := range req.Cookies() { if names[cookie.Name] { return true } names[cookie.Name] = true } return false } func TestVersion0Generates407Status(t *testing.T) { c := qt.New(t) m, err := bakery.NewMacaroon([]byte("root key"), []byte("id"), "location", bakery.Version0, nil) c.Assert(err, qt.IsNil) srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { err := httpbakery.NewDischargeRequiredError(httpbakery.DischargeRequiredErrorParams{ Macaroon: m, }) httpbakery.WriteError(testContext, w, err) })) defer srv.Close() resp, err := http.Get(srv.URL) c.Assert(err, qt.IsNil) c.Assert(resp.StatusCode, qt.Equals, http.StatusProxyAuthRequired) } func TestVersion1Generates401Status(t *testing.T) { c := qt.New(t) m, err := bakery.NewMacaroon([]byte("root key"), []byte("id"), "location", bakery.Version1, nil) c.Assert(err, qt.IsNil) srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { err := httpbakery.NewDischargeRequiredError(httpbakery.DischargeRequiredErrorParams{ Macaroon: m, }) httpbakery.WriteError(testContext, w, err) })) defer srv.Close() req, err := http.NewRequest("GET", srv.URL, nil) c.Assert(err, qt.IsNil) req.Header.Set(httpbakery.BakeryProtocolHeader, "1") resp, err := http.DefaultClient.Do(req) c.Assert(err, qt.IsNil) c.Assert(resp.StatusCode, qt.Equals, http.StatusUnauthorized) c.Assert(resp.Header.Get("WWW-Authenticate"), qt.Equals, "Macaroon") } func newHTTPDischarger(b *bakery.Bakery, checker httpbakery.ThirdPartyCaveatCheckerP) (bakery.PublicKey, http.Handler) { mux := http.NewServeMux() d := httpbakery.NewDischarger(httpbakery.DischargerParams{ CheckerP: checker, Key: b.Oven.Key(), }) d.AddMuxHandlers(mux, "/") return b.Oven.Key().Public, mux } func TestMacaroonCookieName(t *testing.T) { c := qt.New(t) d := bakerytest.NewDischarger(nil) defer d.Close() checked := make(map[string]bool) checker := checkers.New(nil) checker.Namespace().Register("testns", "") checker.Register("once", "testns", func(ctx context.Context, _, arg string) error { if checked[arg] { return errgo.Newf("caveat %q has already been checked once", arg) } checked[arg] = true return nil }) b := newBakery("loc", nil, checker) // We arrange things so that although we use the same client // (with the same cookie jar), the macaroon verification only // succeeds once, so the client always fetches a new macaroon. caveatSeq := 0 cookieName := "" ts := httptest.NewServer(serverHandler(serverHandlerParams{ bakery: b, mutateError: func(e *httpbakery.Error) { e.Info.CookieNameSuffix = cookieName e.Info.MacaroonPath = "/" }, caveats: func() []checkers.Caveat { caveatSeq++ return []checkers.Caveat{{ Condition: fmt.Sprintf("once %d", caveatSeq), }} }, })) defer ts.Close() client := httpbakery.NewClient() doRequest := func() { req, err := http.NewRequest("GET", ts.URL+"/foo/bar/", nil) c.Assert(err, qt.IsNil) resp, err := client.Do(req) c.Assert(err, qt.IsNil) assertResponse(c, resp, "done") } assertCookieNames := func(names ...string) { u, err := url.Parse(ts.URL) c.Assert(err, qt.IsNil) sort.Strings(names) var gotNames []string for _, c := range client.Jar.Cookies(u) { gotNames = append(gotNames, c.Name) } sort.Strings(gotNames) c.Assert(gotNames, qt.DeepEquals, names) } cookieName = "foo" doRequest() assertCookieNames("macaroon-foo") // Another request with the same cookie name should // overwrite the old cookie. doRequest() assertCookieNames("macaroon-foo") // A subsequent request with a different cookie name // should create a new cookie, but the old one will still // be around. cookieName = "bar" doRequest() assertCookieNames("macaroon-foo", "macaroon-bar") } func TestMacaroonCookiePath(t *testing.T) { c := qt.New(t) b := newBakery("loc", nil, nil) cookiePath := "" ts := httptest.NewServer(serverHandler(serverHandlerParams{ bakery: b, mutateError: func(e *httpbakery.Error) { e.Info.MacaroonPath = cookiePath }, })) defer ts.Close() var client *httpbakery.Client doRequest := func() { req, err := http.NewRequest("GET", ts.URL+"/foo/bar/", nil) c.Assert(err, qt.IsNil) client = httpbakery.NewClient() resp, err := client.Do(req) c.Assert(err, qt.IsNil) assertResponse(c, resp, "done") } assertCookieCount := func(path string, n int) { u, err := url.Parse(ts.URL + path) c.Assert(err, qt.IsNil) c.Assert(client.Jar.Cookies(u), qt.HasLen, n) } cookiePath = "" c.Logf("- cookie path %q", cookiePath) doRequest() assertCookieCount("", 0) assertCookieCount("/foo", 0) assertCookieCount("/foo", 0) assertCookieCount("/foo/", 0) assertCookieCount("/foo/bar/", 1) assertCookieCount("/foo/bar/baz", 1) cookiePath = "/foo/" c.Logf("- cookie path %q", cookiePath) doRequest() assertCookieCount("", 0) assertCookieCount("/foo", 0) assertCookieCount("/foo/", 1) assertCookieCount("/foo/bar/", 1) assertCookieCount("/foo/bar/baz", 1) cookiePath = "/foo" c.Logf("- cookie path %q", cookiePath) doRequest() assertCookieCount("", 0) assertCookieCount("/bar", 0) assertCookieCount("/foo", 1) assertCookieCount("/foo/", 1) assertCookieCount("/foo/bar/", 1) assertCookieCount("/foo/bar/baz", 1) cookiePath = "../" c.Logf("- cookie path %q", cookiePath) doRequest() assertCookieCount("", 0) assertCookieCount("/bar", 0) assertCookieCount("/foo", 0) assertCookieCount("/foo/", 1) assertCookieCount("/foo/bar/", 1) assertCookieCount("/foo/bar/baz", 1) cookiePath = "../bar" c.Logf("- cookie path %q", cookiePath) doRequest() assertCookieCount("", 0) assertCookieCount("/bar", 0) assertCookieCount("/foo", 0) assertCookieCount("/foo/", 0) assertCookieCount("/foo/bar/", 1) assertCookieCount("/foo/bar/baz", 1) assertCookieCount("/foo/baz", 0) assertCookieCount("/foo/baz/", 0) assertCookieCount("/foo/baz/bar", 0) cookiePath = "/" c.Logf("- cookie path %q", cookiePath) doRequest() assertCookieCount("", 1) assertCookieCount("/bar", 1) assertCookieCount("/foo", 1) assertCookieCount("/foo/", 1) assertCookieCount("/foo/bar/", 1) assertCookieCount("/foo/bar/baz", 1) } func TestThirdPartyDischargeRefused(t *testing.T) { c := qt.New(t) d := bakerytest.NewDischarger(nil) d.CheckerP = bakerytest.ConditionParser(func(cond, arg string) ([]checkers.Caveat, error) { return nil, errgo.New("boo! cond " + cond) }) defer d.Close() // Create a target service. b := newBakery("loc", d, nil) ts := httptest.NewServer(serverHandler(serverHandlerParams{ bakery: b, authLocation: d.Location(), })) defer ts.Close() // Create a client request. req, err := http.NewRequest("GET", ts.URL, nil) c.Assert(err, qt.IsNil) client := httpbakery.NewClient() // Make the request to the server. resp, err := client.Do(req) _, ok := errgo.Cause(err).(*httpbakery.DischargeError) c.Assert(ok, qt.Equals, true) c.Assert(err, qt.ErrorMatches, `cannot get discharge from ".*": third party refused discharge: cannot discharge: boo! cond is-ok`) c.Assert(resp, qt.IsNil) } func TestDischargeWithInteractionRequiredError(t *testing.T) { c := qt.New(t) d := bakerytest.NewDischarger(nil) defer d.Close() d.CheckerP = bakerytest.ConditionParser(func(cond, arg string) ([]checkers.Caveat, error) { return nil, &httpbakery.Error{ Code: httpbakery.ErrInteractionRequired, Message: "interaction required", Info: &httpbakery.ErrorInfo{ LegacyVisitURL: "http://0.1.2.3/", LegacyWaitURL: "http://0.1.2.3/", }, } }) // Create a target service. b := newBakery("loc", d, nil) ts := httptest.NewServer(serverHandler(serverHandlerParams{ bakery: b, authLocation: d.Location(), })) defer ts.Close() // Create a client request. req, err := http.NewRequest("GET", ts.URL, nil) c.Assert(err, qt.IsNil) errCannotVisit := errgo.New("cannot visit") client := httpbakery.NewClient() client.AddInteractor(legacyInteractor{ kind: httpbakery.WebBrowserInteractionKind, legacyInteract: func(ctx context.Context, client *httpbakery.Client, location string, visitURL *url.URL) error { return errCannotVisit }, }) // Make the request to the server. resp, err := client.Do(req) c.Assert(err, qt.ErrorMatches, `cannot get discharge from "https://.*": cannot start interactive session: cannot visit`) c.Assert(httpbakery.IsInteractionError(errgo.Cause(err)), qt.Equals, true) ierr, ok := errgo.Cause(err).(*httpbakery.InteractionError) c.Assert(ok, qt.Equals, true) c.Assert(errgo.Cause(ierr.Reason), qt.Equals, errCannotVisit) c.Assert(resp, qt.IsNil) } var interactionRequiredMethodsTests = []struct { about string methods map[string]interface{} interactors []httpbakery.Interactor expectInteractCalls int expectMethod string expectError string }{{ about: "single method", methods: map[string]interface{}{ "test-interactor": "interaction-data", }, interactors: []httpbakery.Interactor{ testInteractor("test-interactor"), }, expectInteractCalls: 1, expectMethod: "test-interactor", }, { about: "two methods, first one not used", methods: map[string]interface{}{ "test-interactor": "interaction-data", }, interactors: []httpbakery.Interactor{ testInteractor("other-interactor"), testInteractor("test-interactor"), }, expectInteractCalls: 1, expectMethod: "test-interactor", }, { about: "two methods, first one takes precedence", methods: map[string]interface{}{ "test-interactor": "interaction-data", "other-interactor": "other-data", }, interactors: []httpbakery.Interactor{ testInteractor("other-interactor"), testInteractor("test-interactor"), }, expectInteractCalls: 1, expectMethod: "other-interactor", }, { about: "two methods, first one takes precedence", methods: map[string]interface{}{ "test-interactor": "interaction-data", "other-interactor": "other-data", }, interactors: []httpbakery.Interactor{ testInteractor("test-interactor"), testInteractor("other-interactor"), }, expectInteractCalls: 1, expectMethod: "test-interactor", }, { about: "two methods, first one returns ErrInteractionMethodNotFound", methods: map[string]interface{}{ "test-interactor": "interaction-data", "other-interactor": "other-data", }, interactors: []httpbakery.Interactor{ interactor{ kind: "test-interactor", interact: func(ctx context.Context, client *httpbakery.Client, location string, interactionRequiredErr *httpbakery.Error) (*httpbakery.DischargeToken, error) { return nil, errgo.WithCausef(nil, httpbakery.ErrInteractionMethodNotFound, "") }, }, testInteractor("other-interactor"), }, expectInteractCalls: 2, expectMethod: "other-interactor", }, { about: "interactor returns error", methods: map[string]interface{}{ "test-interactor": "interaction-data", "other-interactor": "other-data", }, interactors: []httpbakery.Interactor{ interactor{ kind: "test-interactor", interact: func(ctx context.Context, client *httpbakery.Client, location string, interactionRequiredErr *httpbakery.Error) (*httpbakery.DischargeToken, error) { return nil, errgo.New("an error") }, }, testInteractor("other-interactor"), }, expectInteractCalls: 1, expectError: `cannot get discharge from "https://.*": an error`, }, { about: "no supported methods", methods: map[string]interface{}{ "a-interactor": "interaction-data", "b-interactor": "other-data", }, interactors: []httpbakery.Interactor{ testInteractor("c-interactor"), testInteractor("d-interactor"), }, expectError: `cannot get discharge from "https://.*": cannot start interactive session: no supported interaction method`, }, { about: "interactor returns nil token", methods: map[string]interface{}{ "test-interactor": "interaction-data", }, interactors: []httpbakery.Interactor{ interactor{ kind: "test-interactor", interact: func(ctx context.Context, client *httpbakery.Client, location string, interactionRequiredErr *httpbakery.Error) (*httpbakery.DischargeToken, error) { return nil, nil }, }, }, expectInteractCalls: 1, expectError: `cannot get discharge from "https://.*": interaction method returned an empty token`, }, { about: "no interaction methods", methods: map[string]interface{}{ "test-interactor": "interaction-data", }, expectError: `cannot get discharge from "https://.*": cannot start interactive session: interaction required but not possible`, }} func TestInteractionRequiredMethods(t *testing.T) { c := qt.New(t) d := bakerytest.NewDischarger(nil) defer d.Close() checkedWithToken := 0 checkedWithoutToken := 0 interactionKind := "" var serverInteractionMethods map[string]interface{} d.CheckerP = httpbakery.ThirdPartyCaveatCheckerPFunc(func(ctx context.Context, p httpbakery.ThirdPartyCaveatCheckerParams) ([]checkers.Caveat, error) { if p.Token != nil { checkedWithToken++ if p.Token.Kind != "test" { c.Errorf("invalid token value") return nil, errgo.Newf("unexpected token value") } interactionKind = string(p.Token.Value) return nil, nil } checkedWithoutToken++ err := httpbakery.NewInteractionRequiredError(nil, p.Request) for key, val := range serverInteractionMethods { err.SetInteraction(key, val) } return nil, err }) // Create a target service. b := newBakery("loc", d, nil) ts := httptest.NewServer(serverHandler(serverHandlerParams{ bakery: b, authLocation: d.Location(), })) defer ts.Close() for i, test := range interactionRequiredMethodsTests { c.Logf("\ntest %d: %s", i, test.about) interactCalls := 0 checkedWithToken = 0 checkedWithoutToken = 0 interactionKind = "" client := httpbakery.NewClient() for _, in := range test.interactors { in := in client.AddInteractor(interactor{ kind: in.Kind(), interact: func(ctx context.Context, client *httpbakery.Client, location string, interactionRequiredErr *httpbakery.Error) (*httpbakery.DischargeToken, error) { interactCalls++ return in.Interact(ctx, client, location, interactionRequiredErr) }, }) c.Logf("added interactor %q", in.Kind()) } serverInteractionMethods = test.methods // Make the request to the server. req, err := http.NewRequest("GET", ts.URL, nil) c.Assert(err, qt.IsNil) resp, err := client.Do(req) if test.expectError != "" { c.Assert(err, qt.ErrorMatches, test.expectError) c.Assert(resp, qt.IsNil) continue } c.Assert(err, qt.Equals, nil) assertResponse(c, resp, "done") c.Check(interactCalls, qt.Equals, test.expectInteractCalls) c.Check(checkedWithoutToken, qt.Equals, 1) c.Check(checkedWithToken, qt.Equals, 1) c.Check(interactionKind, qt.Equals, test.expectMethod) } } func testInteractor(kind string) httpbakery.Interactor { return interactor{ kind: kind, interact: func(ctx context.Context, client *httpbakery.Client, location string, interactionRequiredErr *httpbakery.Error) (*httpbakery.DischargeToken, error) { return &httpbakery.DischargeToken{ Kind: "test", Value: []byte(kind), }, nil }, } } var dischargeWithVisitURLErrorTests = []struct { about string respond func(http.ResponseWriter) expectError string }{{ about: "error message", respond: func(w http.ResponseWriter) { httpReqServer.WriteError(testContext, w, fmt.Errorf("an error")) }, expectError: `cannot get discharge from ".*": failed to acquire macaroon after waiting: third party refused discharge: an error`, }, { about: "non-JSON error", respond: func(w http.ResponseWriter) { w.Write([]byte("bad response")) }, // TODO fix this unhelpful error message expectError: `cannot get discharge from ".*": cannot unmarshal wait response: unexpected content type text/plain; want application/json; content: bad response`, }} func TestDischargeWithVisitURLError(t *testing.T) { c := qt.New(t) visitor := newVisitHandler(nil) visitSrv := httptest.NewServer(visitor) defer visitSrv.Close() d := bakerytest.NewDischarger(nil) d.CheckerP = bakerytest.ConditionParser(func(cond, arg string) ([]checkers.Caveat, error) { return nil, &httpbakery.Error{ Code: httpbakery.ErrInteractionRequired, Message: "interaction required", Info: &httpbakery.ErrorInfo{ LegacyVisitURL: visitSrv.URL + "/visit", LegacyWaitURL: visitSrv.URL + "/wait", }, } }) defer d.Close() // Create a target service. b := newBakery("loc", d, nil) ts := httptest.NewServer(serverHandler(serverHandlerParams{ bakery: b, authLocation: d.Location(), })) defer ts.Close() for i, test := range dischargeWithVisitURLErrorTests { c.Logf("test %d: %s", i, test.about) visitor.respond = test.respond client := httpbakery.NewClient() client.AddInteractor(legacyInteractor{ kind: httpbakery.WebBrowserInteractionKind, legacyInteract: func(ctx context.Context, client *httpbakery.Client, location string, visitURL *url.URL) error { resp, err := http.Get(visitURL.String()) if err != nil { return err } resp.Body.Close() return nil }, }) // Create a client request. req, err := http.NewRequest("GET", ts.URL, nil) c.Assert(err, qt.IsNil) // Make the request to the server. _, err = client.Do(req) c.Assert(err, qt.ErrorMatches, test.expectError) } } func TestMacaroonsForURL(t *testing.T) { c := qt.New(t) // Create a target service. b := newBakery("loc", nil, nil) m1, err := b.Oven.NewMacaroon(testContext, bakery.LatestVersion, nil, testOp) c.Assert(err, qt.IsNil) m2, err := b.Oven.NewMacaroon(testContext, bakery.LatestVersion, nil, testOp) c.Assert(err, qt.IsNil) u1 := mustParseURL("http://0.1.2.3/") u2 := mustParseURL("http://0.1.2.3/x/") // Create some cookies with different cookie paths. jar, err := cookiejar.New(nil) c.Assert(err, qt.IsNil) httpbakery.SetCookie(jar, u1, nil, macaroon.Slice{m1.M()}) httpbakery.SetCookie(jar, u2, nil, macaroon.Slice{m2.M()}) jar.SetCookies(u1, []*http.Cookie{{ Name: "foo", Path: "/", Value: "ignored", }, { Name: "bar", Path: "/x/", Value: "ignored", }}) // Check that MacaroonsForURL behaves correctly // with both single and multiple cookies. mss := httpbakery.MacaroonsForURL(jar, u1) c.Assert(mss, qt.HasLen, 1) c.Assert(mss[0], qt.HasLen, 1) c.Assert(mss[0][0].Id(), qt.DeepEquals, m1.M().Id()) mss = httpbakery.MacaroonsForURL(jar, u2) checked := make(map[string]int) for _, ms := range mss { checked[string(ms[0].Id())]++ _, err := b.Checker.Auth(ms).Allow(testContext, testOp) c.Assert(err, qt.IsNil) } c.Assert(checked, qt.DeepEquals, map[string]int{ string(m1.M().Id()): 1, string(m2.M().Id()): 1, }) } func TestDoWithCustomError(t *testing.T) { c := qt.New(t) d := bakerytest.NewDischarger(nil) defer d.Close() // Create a target service. b := newBakery("loc", d, nil) type customError struct { CustomError *httpbakery.Error } callCount := 0 handler := func(w http.ResponseWriter, req *http.Request) { callCount++ if _, err := b.Checker.Auth(httpbakery.RequestMacaroons(req)...).Allow(testContext, testOp); err != nil { httprequest.WriteJSON(w, http.StatusTeapot, customError{ CustomError: newDischargeRequiredError(serverHandlerParams{ bakery: b, authLocation: d.Location(), }, err, req).(*httpbakery.Error), }) return } fmt.Fprintf(w, "hello there") } srv := httptest.NewServer(http.HandlerFunc(handler)) defer srv.Close() req, err := http.NewRequest("GET", srv.URL, nil) c.Assert(err, qt.IsNil) // First check that a normal request fails. resp, err := httpbakery.NewClient().Do(req) c.Assert(err, qt.IsNil) defer resp.Body.Close() c.Assert(resp.StatusCode, qt.Equals, http.StatusTeapot) c.Assert(callCount, qt.Equals, 1) callCount = 0 // Then check that a request with a custom error getter succeeds. errorGetter := func(resp *http.Response) error { if resp.StatusCode != http.StatusTeapot { return nil } data, err := ioutil.ReadAll(resp.Body) if err != nil { panic(err) } var respErr customError if err := json.Unmarshal(data, &respErr); err != nil { panic(err) } return respErr.CustomError } resp, err = httpbakery.NewClient().DoWithCustomError(req, errorGetter) c.Assert(err, qt.IsNil) data, err := ioutil.ReadAll(resp.Body) c.Assert(err, qt.IsNil) c.Assert(string(data), qt.Equals, "hello there") c.Assert(callCount, qt.Equals, 2) } func TestHandleError(t *testing.T) { c := qt.New(t) d := bakerytest.NewDischarger(nil) defer d.Close() // Create a target service. b := newBakery("loc", d, nil) srv := httptest.NewServer(serverHandler(serverHandlerParams{ bakery: b, authLocation: "unknown", mutateError: nil, })) defer srv.Close() m, err := b.Oven.NewMacaroon(testContext, bakery.LatestVersion, []checkers.Caveat{{ Location: d.Location(), Condition: "something", }}, testOp) c.Assert(err, qt.IsNil) u, err := url.Parse(srv.URL + "/bar") c.Assert(err, qt.IsNil) respErr := &httpbakery.Error{ Message: "an error", Code: httpbakery.ErrDischargeRequired, Info: &httpbakery.ErrorInfo{ Macaroon: m, MacaroonPath: "/foo", }, } client := httpbakery.NewClient() err = client.HandleError(testContext, u, respErr) c.Assert(err, qt.Equals, nil) // No cookies at the original location. c.Assert(client.Client.Jar.Cookies(u), qt.HasLen, 0) u.Path = "/foo" cookies := client.Client.Jar.Cookies(u) c.Assert(cookies, qt.HasLen, 1) // Check that we can actually make a request // with the newly acquired macaroon cookies. req, err := http.NewRequest("GET", srv.URL+"/foo", nil) c.Assert(err, qt.IsNil) resp, err := client.Do(req) c.Assert(err, qt.IsNil) resp.Body.Close() c.Assert(resp.StatusCode, qt.Equals, http.StatusOK) } func TestNewClientOldServer(t *testing.T) { c := qt.New(t) d := bakerytest.NewDischarger(nil) defer d.Close() // Create a target service. b := newBakery("loc", d, nil) srv := httptest.NewServer(serverHandler(serverHandlerParams{ bakery: b, authLocation: d.Location(), })) defer srv.Close() // Make the request to the server. client := httpbakery.NewClient() req, err := http.NewRequest("GET", srv.URL, nil) c.Assert(err, qt.IsNil) resp, err := client.Do(req) c.Assert(err, qt.IsNil) defer resp.Body.Close() assertResponse(c, resp, "done") } func TestHandleErrorDifferentError(t *testing.T) { c := qt.New(t) berr := &httpbakery.Error{ Message: "an error", Code: "another code", } client := httpbakery.NewClient() err := client.HandleError(testContext, &url.URL{}, berr) c.Assert(err, qt.Equals, berr) } func TestNewCookieExpiresLongExpiryTime(t *testing.T) { c := qt.New(t) t0 := time.Now().Add(30 * time.Minute) b := newBakery("loc", nil, nil) m, err := b.Oven.NewMacaroon(testContext, bakery.LatestVersion, []checkers.Caveat{ checkers.TimeBeforeCaveat(t0), }, testOp) c.Assert(err, qt.IsNil) cookie, err := httpbakery.NewCookie(nil, macaroon.Slice{m.M()}) c.Assert(err, qt.IsNil) c.Assert(cookie.Expires.Equal(t0), qt.Equals, true, qt.Commentf("got %s want %s", cookie.Expires, t)) } func TestNewCookieExpiresAlreadyExpired(t *testing.T) { c := qt.New(t) t0 := time.Now().Add(-time.Minute) b := newBakery("loc", nil, nil) m, err := b.Oven.NewMacaroon(testContext, bakery.LatestVersion, []checkers.Caveat{ checkers.TimeBeforeCaveat(t0), }, testOp) c.Assert(err, qt.IsNil) cookie, err := httpbakery.NewCookie(nil, macaroon.Slice{m.M()}) c.Assert(err, qt.IsNil) c.Assert(cookie.Expires, qt.Satisfies, time.Time.IsZero) } func TestNewCookieExpiresNoTimeBeforeCaveat(t *testing.T) { c := qt.New(t) t0 := time.Now() b := newBakery("loc", nil, nil) m, err := b.Oven.NewMacaroon(testContext, bakery.LatestVersion, nil, testOp) c.Assert(err, qt.IsNil) cookie, err := httpbakery.NewCookie(nil, macaroon.Slice{m.M()}) c.Assert(err, qt.IsNil) minExpires := t0.Add(httpbakery.PermanentExpiryDuration) maxExpires := time.Now().Add(httpbakery.PermanentExpiryDuration) if cookie.Expires.Before(minExpires) || cookie.Expires.After(maxExpires) { c.Fatalf("unexpected expiry time; got %v want %v", cookie.Expires, minExpires) } } func mustParseURL(s string) *url.URL { u, err := url.Parse(s) if err != nil { panic(err) } return u } type visitHandler struct { mux *http.ServeMux rendez chan struct{} respond func(w http.ResponseWriter) } func newVisitHandler(respond func(http.ResponseWriter)) *visitHandler { h := &visitHandler{ rendez: make(chan struct{}, 1), respond: respond, mux: http.NewServeMux(), } h.mux.HandleFunc("/visit", h.serveVisit) h.mux.HandleFunc("/wait", h.serveWait) return h } func (h *visitHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { h.mux.ServeHTTP(w, req) } func (h *visitHandler) serveVisit(w http.ResponseWriter, req *http.Request) { h.rendez <- struct{}{} } func (h *visitHandler) serveWait(w http.ResponseWriter, req *http.Request) { <-h.rendez h.respond(w) } // assertResponse asserts that the given response is OK and contains // the expected body text. func assertResponse(c *qt.C, resp *http.Response, expectBody string) { body, err := ioutil.ReadAll(resp.Body) c.Assert(err, qt.IsNil) resp.Body.Close() c.Assert(resp.StatusCode, qt.Equals, http.StatusOK, qt.Commentf("body %q", body)) c.Assert(string(body), qt.DeepEquals, expectBody) resp.Body = ioutil.NopCloser(bytes.NewReader(body)) } type readCounter struct { io.ReadSeeker byteCount int } func (r *readCounter) Read(buf []byte) (int, error) { n, err := r.ReadSeeker.Read(buf) r.byteCount += n return n, err } func newBakery(location string, locator bakery.ThirdPartyLocator, checker bakery.FirstPartyCaveatChecker) *bakery.Bakery { if checker == nil { c := checkers.New(nil) c.Namespace().Register("testns", "") c.Register("is", "testns", checkIsSomething) checker = c } key, err := bakery.GenerateKey() if err != nil { panic(err) } return bakery.New(bakery.BakeryParams{ Location: location, Locator: locator, Key: key, Checker: checker, }) } func clientRequestWithCookies(c *qt.C, u string, macaroons macaroon.Slice) *http.Client { client := httpbakery.NewHTTPClient() url, err := url.Parse(u) c.Assert(err, qt.IsNil) err = httpbakery.SetCookie(client.Jar, url, nil, macaroons) c.Assert(err, qt.IsNil) return client } var httpReqServer = &httprequest.Server{ ErrorMapper: httpbakery.ErrorToResponse, } type serverHandlerParams struct { // bakery is used to check incoming requests // and macaroons for discharge-required errors. bakery *bakery.Bakery // authLocation holds the location of any 3rd party authorizer. // If this is non-empty, a 3rd party caveat will be added // addressed to this location. authLocation string // mutateError, if non-zero, will be called with any // discharge-required error before responding // to the client. mutateError func(*httpbakery.Error) // If caveats is non-nil, it is called to get caveats to // add to the returned macaroon. caveats func() []checkers.Caveat // alwaysReadBody specifies whether the handler should always read // the entire request body before returning. alwaysReadBody bool } // serverHandler returns an HTTP handler that checks macaroon authorization // and, if that succeeds, writes the string "done" followed by all the // data read from the request body. // It recognises the single first party caveat "is something". func serverHandler(hp serverHandlerParams) http.Handler { h := httpReqServer.HandleErrors(func(p httprequest.Params) error { if hp.alwaysReadBody { defer ioutil.ReadAll(p.Request.Body) } if _, err := hp.bakery.Checker.Auth(httpbakery.RequestMacaroons(p.Request)...).Allow(p.Context, testOp); err != nil { return newDischargeRequiredError(hp, err, p.Request) } fmt.Fprintf(p.Response, "done") // Special case: the no-body path doesn't return the body. if p.Request.URL.Path == "/no-body" { return nil } data, err := ioutil.ReadAll(p.Request.Body) if err != nil { panic(fmt.Errorf("cannot read body: %v", err)) } if len(data) > 0 { fmt.Fprintf(p.Response, " %s", data) } return nil }) return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { h(w, req, nil) }) } // newDischargeRequiredError returns a discharge-required error holding // a newly minted macaroon referencing the original check error // checkErr. If hp.authLocation is non-empty, the issued macaroon will // contain an "is-ok" third party caveat addressed to that location. func newDischargeRequiredError(hp serverHandlerParams, checkErr error, req *http.Request) error { var caveats []checkers.Caveat if hp.authLocation != "" { caveats = []checkers.Caveat{{ Location: hp.authLocation, Condition: "is-ok", }} } if hp.caveats != nil { caveats = append(caveats, hp.caveats()...) } m, err := hp.bakery.Oven.NewMacaroon(testContext, bakery.LatestVersion, caveats, testOp) if err != nil { panic(fmt.Errorf("cannot make new macaroon: %v", err)) } err = httpbakery.NewDischargeRequiredError(httpbakery.DischargeRequiredErrorParams{ Macaroon: m, OriginalError: checkErr, Request: req, }) if hp.mutateError != nil { hp.mutateError(err.(*httpbakery.Error)) } return err } func isSomethingCaveat() checkers.Caveat { return checkers.Caveat{ Condition: "is something", Namespace: "testns", } } func checkIsSomething(ctx context.Context, _, arg string) error { if arg != "something" { return fmt.Errorf(`%v doesn't match "something"`, arg) } return nil } type interactor struct { kind string interact func(ctx context.Context, client *httpbakery.Client, location string, interactionRequiredErr *httpbakery.Error) (*httpbakery.DischargeToken, error) } func (i interactor) Kind() string { return i.kind } func (i interactor) Interact(ctx context.Context, client *httpbakery.Client, location string, interactionRequiredErr *httpbakery.Error) (*httpbakery.DischargeToken, error) { return i.interact(ctx, client, location, interactionRequiredErr) } var ( _ httpbakery.Interactor = interactor{} _ httpbakery.Interactor = legacyInteractor{} _ httpbakery.LegacyInteractor = legacyInteractor{} ) type legacyInteractor struct { kind string interact func(ctx context.Context, client *httpbakery.Client, location string, interactionRequiredErr *httpbakery.Error) (*httpbakery.DischargeToken, error) legacyInteract func(ctx context.Context, client *httpbakery.Client, location string, visitURL *url.URL) error } func (i legacyInteractor) Kind() string { return i.kind } func (i legacyInteractor) Interact(ctx context.Context, client *httpbakery.Client, location string, interactionRequiredErr *httpbakery.Error) (*httpbakery.DischargeToken, error) { if i.interact == nil { return nil, errgo.Newf("non-legacy interaction not supported") } return i.interact(ctx, client, location, interactionRequiredErr) } func (i legacyInteractor) LegacyInteract(ctx context.Context, client *httpbakery.Client, location string, visitURL *url.URL) error { if i.legacyInteract == nil { return errgo.Newf("legacy interaction not supported") } return i.legacyInteract(ctx, client, location, visitURL) } macaroon-bakery-3.0.2/httpbakery/context_go17.go000066400000000000000000000002351464427415500216270ustar00rootroot00000000000000// +build go1.7 package httpbakery import ( "context" "net/http" ) func contextFromRequest(req *http.Request) context.Context { return req.Context() } macaroon-bakery-3.0.2/httpbakery/context_prego17.go000066400000000000000000000002451464427415500223370ustar00rootroot00000000000000// +build !go1.7 package httpbakery import ( "context" "net/http" ) func contextFromRequest(req *http.Request) context.Context { return context.Background() } macaroon-bakery-3.0.2/httpbakery/discharge.go000066400000000000000000000302541464427415500212430ustar00rootroot00000000000000package 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 } macaroon-bakery-3.0.2/httpbakery/dischargeclient_generated.go000066400000000000000000000017151464427415500244600ustar00rootroot00000000000000// The code in this file was automatically generated by running httprequest-generate-client. // DO NOT EDIT package httpbakery import ( "context" "gopkg.in/httprequest.v1" ) type dischargeClient struct { Client httprequest.Client } // Discharge discharges a third party caveat. func (c *dischargeClient) Discharge(ctx context.Context, p *dischargeRequest) (*dischargeResponse, error) { var r *dischargeResponse err := c.Client.Call(ctx, p, &r) return r, err } // DischargeInfo returns information on the discharger. func (c *dischargeClient) DischargeInfo(ctx context.Context, p *dischargeInfoRequest) (dischargeInfoResponse, error) { var r dischargeInfoResponse err := c.Client.Call(ctx, p, &r) return r, err } // PublicKey returns the public key of the discharge service. func (c *dischargeClient) PublicKey(ctx context.Context, p *publicKeyRequest) (publicKeyResponse, error) { var r publicKeyResponse err := c.Client.Call(ctx, p, &r) return r, err } macaroon-bakery-3.0.2/httpbakery/error.go000066400000000000000000000264121464427415500204440ustar00rootroot00000000000000package 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 } macaroon-bakery-3.0.2/httpbakery/error_test.go000066400000000000000000000164461464427415500215110ustar00rootroot00000000000000package httpbakery_test import ( "encoding/json" "errors" "net/http" "net/http/httptest" "reflect" "testing" qt "github.com/frankban/quicktest" "github.com/google/go-cmp/cmp" "github.com/juju/qthttptest" "gopkg.in/httprequest.v1" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" "github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery" ) func TestWriteDischargeRequiredError(t *testing.T) { c := qt.New(t) m, err := bakery.NewMacaroon([]byte("secret"), []byte("id"), "a location", bakery.LatestVersion, nil) c.Assert(err, qt.IsNil) tests := []struct { about string path string requestPath string cookieNameSuffix string err error expectedResponse httpbakery.Error }{{ about: `write discharge required with "an error" but no path`, path: "", err: errors.New("an error"), expectedResponse: httpbakery.Error{ Code: httpbakery.ErrDischargeRequired, Message: "an error", Info: &httpbakery.ErrorInfo{ Macaroon: m, MacaroonPath: "/", CookieNameSuffix: "auth", }, }, }, { about: `write discharge required with "an error" but and set a path`, path: "/foo", err: errors.New("an error"), expectedResponse: httpbakery.Error{ Code: httpbakery.ErrDischargeRequired, Message: "an error", Info: &httpbakery.ErrorInfo{ Macaroon: m, MacaroonPath: "/foo", CookieNameSuffix: "auth", }, }, }, { about: `write discharge required with nil error but set a path`, path: "/foo", expectedResponse: httpbakery.Error{ Code: httpbakery.ErrDischargeRequired, Message: httpbakery.ErrDischargeRequired.Error(), Info: &httpbakery.ErrorInfo{ Macaroon: m, MacaroonPath: "/foo", CookieNameSuffix: "auth", }, }, }, { about: `empty cookie path`, requestPath: "/foo/bar/baz", expectedResponse: httpbakery.Error{ Code: httpbakery.ErrDischargeRequired, Message: httpbakery.ErrDischargeRequired.Error(), Info: &httpbakery.ErrorInfo{ Macaroon: m, MacaroonPath: "../../", CookieNameSuffix: "auth", }, }, }, { about: `specified cookie name suffix`, cookieNameSuffix: "some-name", expectedResponse: httpbakery.Error{ Code: httpbakery.ErrDischargeRequired, Message: httpbakery.ErrDischargeRequired.Error(), Info: &httpbakery.ErrorInfo{ Macaroon: m, MacaroonPath: "/", CookieNameSuffix: "some-name", }, }, }} for i, t := range tests { c.Logf("test %d: %s", i, t.about) var req *http.Request if t.requestPath != "" { req0, err := http.NewRequest("GET", t.requestPath, nil) c.Check(err, qt.Equals, nil) req = req0 } response := httptest.NewRecorder() err := httpbakery.NewDischargeRequiredError(httpbakery.DischargeRequiredErrorParams{ Macaroon: m, CookiePath: t.path, OriginalError: t.err, CookieNameSuffix: t.cookieNameSuffix, Request: req, }) httpbakery.WriteError(testContext, response, err) qthttptest.AssertJSONResponse(c, response, http.StatusUnauthorized, t.expectedResponse) } } func TestNewInteractionRequiredError(t *testing.T) { c := qt.New(t) // With a request with no version header, the response // should be 407. req, err := http.NewRequest("GET", "/", nil) c.Assert(err, qt.IsNil) err = httpbakery.NewInteractionRequiredError(nil, req) code, resp := httpbakery.ErrorToResponse(testContext, err) c.Assert(code, qt.Equals, http.StatusProxyAuthRequired) data, err := json.Marshal(resp) c.Assert(err, qt.IsNil) c.Assert(string(data), qthttptest.JSONEquals, &httpbakery.Error{ Code: httpbakery.ErrInteractionRequired, Message: httpbakery.ErrInteractionRequired.Error(), }) // With a request with a version 1 header, the response // should be 401. req.Header.Set("Bakery-Protocol-Version", "1") err = httpbakery.NewInteractionRequiredError(nil, req) code, resp = httpbakery.ErrorToResponse(testContext, err) c.Assert(code, qt.Equals, http.StatusUnauthorized) h := make(http.Header) resp.(httprequest.HeaderSetter).SetHeader(h) c.Assert(h.Get("WWW-Authenticate"), qt.Equals, "Macaroon") data, err = json.Marshal(resp) c.Assert(err, qt.IsNil) c.Assert(string(data), qthttptest.JSONEquals, &httpbakery.Error{ Code: httpbakery.ErrInteractionRequired, Message: httpbakery.ErrInteractionRequired.Error(), }) // With a request with a later version header, the response // should be also be 401. req.Header.Set("Bakery-Protocol-Version", "2") err = httpbakery.NewInteractionRequiredError(nil, req) code, resp = httpbakery.ErrorToResponse(testContext, err) c.Assert(code, qt.Equals, http.StatusUnauthorized) h = make(http.Header) resp.(httprequest.HeaderSetter).SetHeader(h) c.Assert(h.Get("WWW-Authenticate"), qt.Equals, "Macaroon") data, err = json.Marshal(resp) c.Assert(err, qt.IsNil) c.Assert(string(data), qthttptest.JSONEquals, &httpbakery.Error{ Code: httpbakery.ErrInteractionRequired, Message: httpbakery.ErrInteractionRequired.Error(), }) } func TestSetInteraction(t *testing.T) { c := qt.New(t) var e httpbakery.Error e.SetInteraction("foo", 5) c.Assert(e, qt.CmpEquals(cmp.AllowUnexported(httpbakery.Error{})), httpbakery.Error{ Info: &httpbakery.ErrorInfo{ InteractionMethods: map[string]*json.RawMessage{ "foo": jsonRawMessage("5"), }, }, }) } func jsonRawMessage(s string) *json.RawMessage { m := json.RawMessage(s) return &m } var interactionMethodTests = []struct { about string err *httpbakery.Error kind string expect interface{} expectError string }{{ about: "no info", err: &httpbakery.Error{}, expect: 0, expectError: `not an interaction-required error \(code \)`, }, { about: "not interaction-required code", err: &httpbakery.Error{ Code: "other", Info: &httpbakery.ErrorInfo{}, }, expect: 0, expectError: `not an interaction-required error \(code other\)`, }, { about: "interaction method not found", err: &httpbakery.Error{ Code: httpbakery.ErrInteractionRequired, Info: &httpbakery.ErrorInfo{ InteractionMethods: map[string]*json.RawMessage{ "foo": jsonRawMessage("0"), }, }, }, kind: "x", expect: 0, expectError: `interaction method "x" not found`, }, { about: "cannot unmarshal", err: &httpbakery.Error{ Code: httpbakery.ErrInteractionRequired, Info: &httpbakery.ErrorInfo{ InteractionMethods: map[string]*json.RawMessage{ "x": jsonRawMessage(`{"X": 45}`), }, }, }, kind: "x", expect: struct { X string }{}, expectError: `cannot unmarshal data for interaction method "x": json: cannot unmarshal number into .* of type string`, }, { about: "success", err: &httpbakery.Error{ Code: httpbakery.ErrInteractionRequired, Info: &httpbakery.ErrorInfo{ InteractionMethods: map[string]*json.RawMessage{ "x": jsonRawMessage(`45`), }, }, }, kind: "x", expect: 45, }} func TestInteractionMethod(t *testing.T) { c := qt.New(t) for i, test := range interactionMethodTests { c.Logf("test %d: %s", i, test.about) v := reflect.New(reflect.TypeOf(test.expect)) err := test.err.InteractionMethod(test.kind, v.Interface()) if test.expectError != "" { c.Assert(err, qt.ErrorMatches, test.expectError) } else { c.Assert(err, qt.Equals, nil) c.Assert(v.Elem().Interface(), qt.DeepEquals, test.expect) } } } macaroon-bakery-3.0.2/httpbakery/export_test.go000066400000000000000000000002551464427415500216700ustar00rootroot00000000000000package httpbakery type PublicKeyResponse publicKeyResponse const MaxDischargeRetries = maxDischargeRetries var LegacyGetInteractionMethods = legacyGetInteractionMethods macaroon-bakery-3.0.2/httpbakery/form/000077500000000000000000000000001464427415500177225ustar00rootroot00000000000000macaroon-bakery-3.0.2/httpbakery/form/form.go000066400000000000000000000123251464427415500212170ustar00rootroot00000000000000// Package form enables interactive login without using a web browser. package form import ( "context" "net/url" "golang.org/x/net/publicsuffix" "gopkg.in/errgo.v1" "gopkg.in/httprequest.v1" "gopkg.in/juju/environschema.v1" "gopkg.in/juju/environschema.v1/form" "github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery" ) /* PROTOCOL A form login works as follows: Client Login Service | | | Discharge request | |----------------------------------->| | | | Interaction-required error with | | "form" entry with formURL. | |<-----------------------------------| | | | GET "form" URL | |----------------------------------->| | | | Schema definition | |<-----------------------------------| | | +-------------+ | | Client | | | Interaction | | +-------------+ | | | | POST data to "form" URL | |----------------------------------->| | | | Form login response | | with discharge token | |<-----------------------------------| | | | Discharge request with | | discharge token. | |----------------------------------->| | | | Discharge macaroon | |<-----------------------------------| The schema is provided as a environschema.Fields object. It is the client's responsibility to interpret the schema and present it to the user. */ const ( // InteractionMethod is the methodURLs key // used for a URL that can be used for form-based // interaction. InteractionMethod = "form" ) // SchemaResponse contains the message expected in response to the schema // request. type SchemaResponse struct { Schema environschema.Fields `json:"schema"` } // InteractionInfo holds the information expected in // the form interaction entry in an interaction-required // error. type InteractionInfo struct { URL string `json:"url"` } // LoginRequest is a request to perform a login using the provided form. type LoginRequest struct { httprequest.Route `httprequest:"POST"` Body LoginBody `httprequest:",body"` } // LoginBody holds the body of a form login request. type LoginBody struct { Form map[string]interface{} `json:"form"` } type LoginResponse struct { Token *httpbakery.DischargeToken `json:"token"` } // Interactor implements httpbakery.Interactor // by providing form-based interaction. type Interactor struct { // Filler holds the form filler that will be used when // form-based interaction is required. Filler form.Filler } // Kind implements httpbakery.Interactor.Kind. func (i Interactor) Kind() string { return InteractionMethod } // Interact implements httpbakery.Interactor.Interact. func (i Interactor) Interact(ctx context.Context, client *httpbakery.Client, location string, interactionRequiredErr *httpbakery.Error) (*httpbakery.DischargeToken, error) { var p InteractionInfo if err := interactionRequiredErr.InteractionMethod(InteractionMethod, &p); err != nil { return nil, errgo.Mask(err) } if p.URL == "" { return nil, errgo.Newf("no URL found in form information") } schemaURL, err := relativeURL(location, p.URL) if err != nil { return nil, errgo.Notef(err, "invalid url %q", p.URL) } httpReqClient := &httprequest.Client{ Doer: client, } var s SchemaResponse if err := httpReqClient.Get(ctx, schemaURL.String(), &s); err != nil { return nil, errgo.Notef(err, "cannot get schema") } if len(s.Schema) == 0 { return nil, errgo.Newf("invalid schema: no fields found") } host, err := publicsuffix.EffectiveTLDPlusOne(schemaURL.Host) if err != nil { host = schemaURL.Host } formValues, err := i.Filler.Fill(form.Form{ Title: "Log in to " + host, Fields: s.Schema, }) if err != nil { return nil, errgo.NoteMask(err, "cannot handle form", errgo.Any) } lr := LoginRequest{ Body: LoginBody{ Form: formValues, }, } var lresp LoginResponse if err := httpReqClient.CallURL(ctx, schemaURL.String(), &lr, &lresp); err != nil { return nil, errgo.Notef(err, "cannot submit form") } if lresp.Token == nil { return nil, errgo.Newf("no token found in form response") } return lresp.Token, nil } // relativeURL returns newPath relative to an original URL. func relativeURL(base, new string) (*url.URL, error) { if new == "" { return nil, errgo.Newf("empty URL") } baseURL, err := url.Parse(base) if err != nil { return nil, errgo.Notef(err, "cannot parse URL") } newURL, err := url.Parse(new) if err != nil { return nil, errgo.Notef(err, "cannot parse URL") } return baseURL.ResolveReference(newURL), nil } macaroon-bakery-3.0.2/httpbakery/form/form_test.go000066400000000000000000000214261464427415500222600ustar00rootroot00000000000000package form_test import ( "context" "net/http" "testing" qt "github.com/frankban/quicktest" "github.com/juju/qthttptest" "gopkg.in/errgo.v1" "gopkg.in/httprequest.v1" "gopkg.in/juju/environschema.v1" esform "gopkg.in/juju/environschema.v1/form" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/checkers" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/identchecker" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakerytest" "github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery" "github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery/form" ) var reqServer = httprequest.Server{ ErrorMapper: httpbakery.ErrorToResponse, } var formLoginTests = []struct { about string filler fillerFunc expectError string noFormMethod bool getForm func() (environschema.Fields, error) postForm func(values map[string]interface{}) (*httpbakery.DischargeToken, error) }{{ about: "complete visit", getForm: func() (environschema.Fields, error) { return userPassForm, nil }, postForm: func(values map[string]interface{}) (*httpbakery.DischargeToken, error) { return &httpbakery.DischargeToken{ Kind: "form", Value: []byte("ok"), }, nil }, }, { about: "error getting schema", getForm: func() (environschema.Fields, error) { return nil, errgo.Newf("some error") }, expectError: `cannot get discharge from ".*": cannot get schema: Get https://.*/form: some error`, }, { about: "form visit method not supported", noFormMethod: true, expectError: `cannot get discharge from ".*": cannot start interactive session: no supported interaction method`, }, { about: "error submitting form", getForm: func() (environschema.Fields, error) { return userPassForm, nil }, postForm: func(values map[string]interface{}) (*httpbakery.DischargeToken, error) { return nil, errgo.Newf("some error") }, expectError: `cannot get discharge from ".*": cannot submit form.*: some error`, }, { about: "no schema", getForm: func() (environschema.Fields, error) { return nil, nil }, expectError: `cannot get discharge from ".*": invalid schema: no fields found`, }, { about: "filler error", getForm: func() (environschema.Fields, error) { return userPassForm, nil }, filler: func(esform.Form) (map[string]interface{}, error) { return nil, errgo.Newf("test error") }, expectError: `cannot get discharge from ".*": cannot handle form: test error`, }, { about: "invalid token returned from form submission", getForm: func() (environschema.Fields, error) { return userPassForm, nil }, postForm: func(values map[string]interface{}) (*httpbakery.DischargeToken, error) { return &httpbakery.DischargeToken{ Kind: "other", Value: []byte("something"), }, nil }, expectError: `cannot get discharge from ".*": Post .*: cannot discharge: invalid token .*`, }} func TestFormLogin(t *testing.T) { c := qt.New(t) var ( getForm func() (environschema.Fields, error) postForm func(values map[string]interface{}) (*httpbakery.DischargeToken, error) noFormMethod bool ) discharger := bakerytest.NewDischarger(nil) defer discharger.Close() discharger.AddHTTPHandlers(FormHandlers(FormHandler{ getForm: func() (environschema.Fields, error) { return getForm() }, postForm: func(values map[string]interface{}) (*httpbakery.DischargeToken, error) { return postForm(values) }, })) discharger.Checker = httpbakery.ThirdPartyCaveatCheckerFunc(func(ctx context.Context, req *http.Request, info *bakery.ThirdPartyCaveatInfo, token *httpbakery.DischargeToken) ([]checkers.Caveat, error) { if token != nil { if token.Kind != "form" || string(token.Value) != "ok" { return nil, errgo.Newf("invalid token %#v", token) } return nil, nil } err := httpbakery.NewInteractionRequiredError(nil, req) if noFormMethod { err.SetInteraction("notform", "value") } else { err.SetInteraction("form", form.InteractionInfo{ URL: "/form", }) } return nil, err }) b := bakery.New(bakery.BakeryParams{ Key: bakery.MustGenerateKey(), Locator: discharger, }) for i, test := range formLoginTests { c.Logf("\ntest %d: %s", i, test.about) getForm = test.getForm postForm = test.postForm noFormMethod = test.noFormMethod m, err := b.Oven.NewMacaroon(context.TODO(), bakery.LatestVersion, []checkers.Caveat{{ Location: discharger.Location(), Condition: "test condition", }}, identchecker.LoginOp) c.Assert(err, qt.Equals, nil) client := httpbakery.NewClient() filler := defaultFiller if test.filler != nil { filler = test.filler } client.AddInteractor(form.Interactor{ Filler: filler, }) ms, err := client.DischargeAll(context.Background(), m) if test.expectError != "" { c.Assert(err, qt.ErrorMatches, test.expectError) continue } c.Assert(err, qt.IsNil) c.Assert(len(ms), qt.Equals, 2) } } var formTitleTests = []struct { host string expect string }{{ host: "xyz.com", expect: "Log in to xyz.com", }, { host: "abc.xyz.com", expect: "Log in to xyz.com", }, { host: "com", expect: "Log in to com", }} func TestFormTitle(t *testing.T) { c := qt.New(t) discharger := bakerytest.NewDischarger(nil) defer discharger.Close() discharger.AddHTTPHandlers(FormHandlers(FormHandler{ getForm: func() (environschema.Fields, error) { return userPassForm, nil }, postForm: func(values map[string]interface{}) (*httpbakery.DischargeToken, error) { return &httpbakery.DischargeToken{ Kind: "form", Value: []byte("ok"), }, nil }, })) discharger.Checker = httpbakery.ThirdPartyCaveatCheckerFunc(func(ctx context.Context, req *http.Request, info *bakery.ThirdPartyCaveatInfo, token *httpbakery.DischargeToken) ([]checkers.Caveat, error) { if token != nil { return nil, nil } err := httpbakery.NewInteractionRequiredError(nil, req) err.SetInteraction("form", form.InteractionInfo{ URL: "/form", }) return nil, err }) b := identchecker.NewBakery(identchecker.BakeryParams{ Key: bakery.MustGenerateKey(), Locator: testLocator{ loc: discharger.Location(), locator: discharger, }, }) for i, test := range formTitleTests { c.Logf("test %d: %s", i, test.host) m, err := b.Oven.NewMacaroon(context.TODO(), bakery.LatestVersion, []checkers.Caveat{{ Location: "https://" + test.host, Condition: "test condition", }}, identchecker.LoginOp) c.Assert(err, qt.Equals, nil) client := httpbakery.NewClient() c.Logf("match %v; replace with %v", test.host, discharger.Location()) client.Client.Transport = qthttptest.URLRewritingTransport{ MatchPrefix: "https://" + test.host, Replace: discharger.Location(), RoundTripper: http.DefaultTransport, } var f titleTestFiller client.AddInteractor(form.Interactor{ Filler: &f, }) ms, err := client.DischargeAll(context.Background(), m) c.Assert(err, qt.IsNil) c.Assert(len(ms), qt.Equals, 2) c.Assert(f.title, qt.Equals, test.expect) } } func FormHandlers(h FormHandler) []httprequest.Handler { return reqServer.Handlers(func(p httprequest.Params) (formHandlers, context.Context, error) { return formHandlers{h}, p.Context, nil }) } type FormHandler struct { getForm func() (environschema.Fields, error) postForm func(values map[string]interface{}) (*httpbakery.DischargeToken, error) } type formHandlers struct { h FormHandler } type schemaRequest struct { httprequest.Route `httprequest:"GET /form"` } func (d formHandlers) GetForm(*schemaRequest) (*form.SchemaResponse, error) { schema, err := d.h.getForm() if err != nil { return nil, errgo.Mask(err) } return &form.SchemaResponse{schema}, nil } type loginRequest struct { httprequest.Route `httprequest:"POST /form"` form.LoginRequest } func (d formHandlers) PostForm(req *loginRequest) (*form.LoginResponse, error) { token, err := d.h.postForm(req.Body.Form) if err != nil { return nil, errgo.Mask(err) } return &form.LoginResponse{ Token: token, }, nil } type fillerFunc func(esform.Form) (map[string]interface{}, error) func (f fillerFunc) Fill(form esform.Form) (map[string]interface{}, error) { return f(form) } var defaultFiller = fillerFunc(func(esform.Form) (map[string]interface{}, error) { return map[string]interface{}{"test": 1}, nil }) type testLocator struct { loc string locator bakery.ThirdPartyLocator } func (l testLocator) ThirdPartyInfo(ctx context.Context, loc string) (bakery.ThirdPartyInfo, error) { return l.locator.ThirdPartyInfo(ctx, l.loc) } type titleTestFiller struct { title string } func (f *titleTestFiller) Fill(form esform.Form) (map[string]interface{}, error) { f.title = form.Title return map[string]interface{}{"test": 1}, nil } var userPassForm = environschema.Fields{ "username": environschema.Attr{ Type: environschema.Tstring, }, "password": environschema.Attr{ Type: environschema.Tstring, Secret: true, }, } macaroon-bakery-3.0.2/httpbakery/keyring.go000066400000000000000000000072471464427415500207700ustar00rootroot00000000000000package httpbakery import ( "context" "net/http" "net/url" "gopkg.in/errgo.v1" "gopkg.in/httprequest.v1" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" ) var _ bakery.ThirdPartyLocator = (*ThirdPartyLocator)(nil) // NewThirdPartyLocator returns a new third party // locator that uses the given client to find // information about third parties and // uses the given cache as a backing. // // If cache is nil, a new cache will be created. // // If client is nil, http.DefaultClient will be used. func NewThirdPartyLocator(client httprequest.Doer, cache *bakery.ThirdPartyStore) *ThirdPartyLocator { if cache == nil { cache = bakery.NewThirdPartyStore() } if client == nil { client = http.DefaultClient } return &ThirdPartyLocator{ client: client, cache: cache, } } // AllowInsecureThirdPartyLocator holds whether ThirdPartyLocator allows // insecure HTTP connections for fetching third party information. // It is provided for testing purposes and should not be used // in production code. var AllowInsecureThirdPartyLocator = false // ThirdPartyLocator represents locator that can interrogate // third party discharge services for information. By default it refuses // to use insecure URLs. type ThirdPartyLocator struct { client httprequest.Doer allowInsecure bool cache *bakery.ThirdPartyStore } // AllowInsecure allows insecure URLs. This can be useful // for testing purposes. See also AllowInsecureThirdPartyLocator. func (kr *ThirdPartyLocator) AllowInsecure() { kr.allowInsecure = true } // ThirdPartyLocator implements bakery.ThirdPartyLocator // by first looking in the backing cache and, if that fails, // making an HTTP request to find the information associated // with the given discharge location. // // It refuses to fetch information from non-HTTPS URLs. func (kr *ThirdPartyLocator) ThirdPartyInfo(ctx context.Context, loc string) (bakery.ThirdPartyInfo, error) { // If the cache has an entry in, we can use it regardless of URL scheme. // This allows entries for notionally insecure URLs to be added by other means (for // example via a config file). info, err := kr.cache.ThirdPartyInfo(ctx, loc) if err == nil { return info, nil } u, err := url.Parse(loc) if err != nil { return bakery.ThirdPartyInfo{}, errgo.Notef(err, "invalid discharge URL %q", loc) } if u.Scheme != "https" && !kr.allowInsecure && !AllowInsecureThirdPartyLocator { return bakery.ThirdPartyInfo{}, errgo.Newf("untrusted discharge URL %q", loc) } info, err = ThirdPartyInfoForLocation(ctx, kr.client, loc) if err != nil { return bakery.ThirdPartyInfo{}, errgo.Mask(err) } kr.cache.AddInfo(loc, info) return info, nil } // ThirdPartyInfoForLocation returns information on the third party // discharge server running at the given location URL. Note that this is // insecure if an http: URL scheme is used. If client is nil, // http.DefaultClient will be used. func ThirdPartyInfoForLocation(ctx context.Context, client httprequest.Doer, url string) (bakery.ThirdPartyInfo, error) { dclient := newDischargeClient(url, client) info, err := dclient.DischargeInfo(ctx, &dischargeInfoRequest{}) if err == nil { return bakery.ThirdPartyInfo{ PublicKey: *info.PublicKey, Version: info.Version, }, nil } derr, ok := errgo.Cause(err).(*httprequest.DecodeResponseError) if !ok || derr.Response.StatusCode != http.StatusNotFound { return bakery.ThirdPartyInfo{}, errgo.Mask(err) } // The new endpoint isn't there, so try the old one. pkResp, err := dclient.PublicKey(ctx, &publicKeyRequest{}) if err != nil { return bakery.ThirdPartyInfo{}, errgo.Mask(err) } return bakery.ThirdPartyInfo{ PublicKey: *pkResp.PublicKey, Version: bakery.Version1, }, nil } macaroon-bakery-3.0.2/httpbakery/keyring_test.go000066400000000000000000000154501464427415500220220ustar00rootroot00000000000000package httpbakery_test import ( "fmt" "net/http" "net/http/httptest" "net/http/httputil" "net/url" "sync" "testing" qt "github.com/frankban/quicktest" "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/bakerytest" "github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery" ) func TestCachePrepopulated(t *testing.T) { c := qt.New(t) cache := bakery.NewThirdPartyStore() key, err := bakery.GenerateKey() c.Assert(err, qt.IsNil) expectInfo := bakery.ThirdPartyInfo{ PublicKey: key.Public, Version: bakery.LatestVersion, } cache.AddInfo("https://0.1.2.3/", expectInfo) kr := httpbakery.NewThirdPartyLocator(nil, cache) info, err := kr.ThirdPartyInfo(testContext, "https://0.1.2.3/") c.Assert(err, qt.IsNil) c.Assert(info, qt.DeepEquals, expectInfo) } func TestCachePrepopulatedInsecure(t *testing.T) { c := qt.New(t) // We allow an insecure URL in a prepopulated cache. cache := bakery.NewThirdPartyStore() key, err := bakery.GenerateKey() c.Assert(err, qt.Equals, nil) expectInfo := bakery.ThirdPartyInfo{ PublicKey: key.Public, Version: bakery.LatestVersion, } cache.AddInfo("http://0.1.2.3/", expectInfo) kr := httpbakery.NewThirdPartyLocator(nil, cache) info, err := kr.ThirdPartyInfo(testContext, "http://0.1.2.3/") c.Assert(err, qt.Equals, nil) c.Assert(info, qt.DeepEquals, expectInfo) } func TestCacheMiss(t *testing.T) { c := qt.New(t) d := bakerytest.NewDischarger(nil) defer d.Close() kr := httpbakery.NewThirdPartyLocator(nil, nil) expectInfo := bakery.ThirdPartyInfo{ PublicKey: d.Key.Public, Version: bakery.LatestVersion, } location := d.Location() info, err := kr.ThirdPartyInfo(testContext, location) c.Assert(err, qt.IsNil) c.Assert(info, qt.DeepEquals, expectInfo) // Close down the service and make sure that // the key is cached. d.Close() info, err = kr.ThirdPartyInfo(testContext, location) c.Assert(err, qt.IsNil) c.Assert(info, qt.DeepEquals, expectInfo) } func TestInsecureURL(t *testing.T) { c := qt.New(t) // Set up a discharger with an non-HTTPS access point. d := bakerytest.NewDischarger(nil) defer d.Close() httpsDischargeURL, err := url.Parse(d.Location()) c.Assert(err, qt.IsNil) srv := httptest.NewServer(httputil.NewSingleHostReverseProxy(httpsDischargeURL)) defer srv.Close() // Check that we are refused because it's an insecure URL. kr := httpbakery.NewThirdPartyLocator(nil, nil) info, err := kr.ThirdPartyInfo(testContext, srv.URL) c.Assert(err, qt.ErrorMatches, `untrusted discharge URL "http://.*"`) c.Assert(info, qt.DeepEquals, bakery.ThirdPartyInfo{}) // Check that it does work when we've enabled AllowInsecure. kr.AllowInsecure() info, err = kr.ThirdPartyInfo(testContext, srv.URL) c.Assert(err, qt.IsNil) c.Assert(info, qt.DeepEquals, bakery.ThirdPartyInfo{ PublicKey: d.Key.Public, Version: bakery.LatestVersion, }) } func TestConcurrentThirdPartyInfo(t *testing.T) { c := qt.New(t) // This test is designed to fail only if run with the race detector // enabled. d := bakerytest.NewDischarger(nil) defer d.Close() kr := httpbakery.NewThirdPartyLocator(nil, nil) var wg sync.WaitGroup for i := 0; i < 2; i++ { wg.Add(1) go func() { _, err := kr.ThirdPartyInfo(testContext, d.Location()) c.Check(err, qt.IsNil) defer wg.Done() }() } wg.Wait() } func TestCustomHTTPClient(t *testing.T) { c := qt.New(t) client := &http.Client{ Transport: errorTransport{}, } kr := httpbakery.NewThirdPartyLocator(client, nil) info, err := kr.ThirdPartyInfo(testContext, "https://0.1.2.3/") c.Assert(err, qt.ErrorMatches, `(Get|GET) ["]?https://0.1.2.3/discharge/info["]?: custom round trip error`) c.Assert(info, qt.DeepEquals, bakery.ThirdPartyInfo{}) } func TestThirdPartyInfoForLocation(t *testing.T) { c := qt.New(t) d := bakerytest.NewDischarger(nil) defer d.Close() client := httpbakery.NewHTTPClient() info, err := httpbakery.ThirdPartyInfoForLocation(testContext, client, d.Location()) c.Assert(err, qt.IsNil) expectedInfo := bakery.ThirdPartyInfo{ PublicKey: d.Key.Public, Version: bakery.LatestVersion, } c.Assert(info, qt.DeepEquals, expectedInfo) // Check that it works with client==nil. info, err = httpbakery.ThirdPartyInfoForLocation(testContext, nil, d.Location()) c.Assert(err, qt.IsNil) c.Assert(info, qt.DeepEquals, expectedInfo) } func TestThirdPartyInfoForLocationWrongURL(t *testing.T) { c := qt.New(t) client := httpbakery.NewHTTPClient() _, err := httpbakery.ThirdPartyInfoForLocation(testContext, client, "http://localhost:0") c.Logf("%v", errgo.Details(err)) c.Assert(err, qt.ErrorMatches, `(Get|GET) ["]?http://localhost:0/discharge/info["]?: dial tcp (127.0.0.1|\[::1\]):0: .*connection refused`) } func TestThirdPartyInfoForLocationReturnsInvalidJSON(t *testing.T) { c := qt.New(t) ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, "BADJSON") })) defer ts.Close() client := httpbakery.NewHTTPClient() _, err := httpbakery.ThirdPartyInfoForLocation(testContext, client, ts.URL) c.Assert(err, qt.ErrorMatches, fmt.Sprintf(`Get ["]?http://.*/discharge/info["]?: unexpected content type text/plain; want application/json; content: BADJSON`)) } func TestThirdPartyInfoForLocationReturnsStatusInternalServerError(t *testing.T) { c := qt.New(t) ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusInternalServerError) })) defer ts.Close() client := httpbakery.NewHTTPClient() _, err := httpbakery.ThirdPartyInfoForLocation(testContext, client, ts.URL) c.Assert(err, qt.ErrorMatches, `Get .*/discharge/info: cannot unmarshal error response \(status 500 Internal Server Error\): unexpected content type .*`) } func TestThirdPartyInfoForLocationFallbackToOldVersion(t *testing.T) { c := qt.New(t) // Start a bakerytest discharger so we benefit from its TLS-verification-skip logic. d := bakerytest.NewDischarger(nil) defer d.Close() key, err := bakery.GenerateKey() c.Assert(err, qt.IsNil) // Start a server which serves the publickey endpoint only. mux := http.NewServeMux() server := httptest.NewTLSServer(mux) mux.HandleFunc("/publickey", func(w http.ResponseWriter, req *http.Request) { c.Check(req.Method, qt.Equals, "GET") httprequest.WriteJSON(w, http.StatusOK, &httpbakery.PublicKeyResponse{ PublicKey: &key.Public, }) }) info, err := httpbakery.ThirdPartyInfoForLocation(testContext, httpbakery.NewHTTPClient(), server.URL) c.Assert(err, qt.IsNil) expectedInfo := bakery.ThirdPartyInfo{ PublicKey: key.Public, Version: bakery.Version1, } c.Assert(info, qt.DeepEquals, expectedInfo) } type errorTransport struct{} func (errorTransport) RoundTrip(req *http.Request) (*http.Response, error) { return nil, errgo.New("custom round trip error") } macaroon-bakery-3.0.2/httpbakery/oven.go000066400000000000000000000056251464427415500202650ustar00rootroot00000000000000package httpbakery import ( "context" "net/http" "time" "gopkg.in/errgo.v1" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/checkers" ) // Oven is like bakery.Oven except it provides a method for // translating errors returned by bakery.AuthChecker into // errors suitable for passing to WriteError. type Oven struct { // Oven holds the bakery Oven used to create // new macaroons to put in discharge-required errors. *bakery.Oven // AuthnExpiry holds the expiry time of macaroons that // are created for authentication. As these are generally // applicable to all endpoints in an API, this is usually // longer than AuthzExpiry. If this is zero, DefaultAuthnExpiry // will be used. AuthnExpiry time.Duration // AuthzExpiry holds the expiry time of macaroons that are // created for authorization. As these are generally applicable // to specific operations, they generally don't need // a long lifespan, so this is usually shorter than AuthnExpiry. // If this is zero, DefaultAuthzExpiry will be used. AuthzExpiry time.Duration } // Default expiry times for macaroons created by Oven.Error. const ( DefaultAuthnExpiry = 7 * 24 * time.Hour DefaultAuthzExpiry = 5 * time.Minute ) // Error processes an error as returned from bakery.AuthChecker // into an error suitable for returning as a response to req // with WriteError. // // Specifically, it translates bakery.ErrPermissionDenied into // ErrPermissionDenied and bakery.DischargeRequiredError // into an Error with an ErrDischargeRequired code, using // oven.Oven to mint the macaroon in it. func (oven *Oven) Error(ctx context.Context, req *http.Request, err error) error { cause := errgo.Cause(err) if cause == bakery.ErrPermissionDenied { return errgo.WithCausef(err, ErrPermissionDenied, "") } derr, ok := cause.(*bakery.DischargeRequiredError) if !ok { return errgo.Mask(err) } // TODO it's possible to have more than two levels here - think // about some naming scheme for the cookies that allows that. expiryDuration := oven.AuthzExpiry if expiryDuration == 0 { expiryDuration = DefaultAuthzExpiry } cookieName := "authz" if derr.ForAuthentication { // Authentication macaroons are a bit different, so use // a different cookie name so both can be presented together. cookieName = "authn" expiryDuration = oven.AuthnExpiry if expiryDuration == 0 { expiryDuration = DefaultAuthnExpiry } } m, err := oven.Oven.NewMacaroon(ctx, RequestVersion(req), derr.Caveats, derr.Ops...) if err != nil { return errgo.Notef(err, "cannot mint new macaroon") } if err := m.AddCaveat(ctx, checkers.TimeBeforeCaveat(time.Now().Add(expiryDuration)), nil, nil); err != nil { return errgo.Notef(err, "cannot add time-before caveat") } return NewDischargeRequiredError(DischargeRequiredErrorParams{ Macaroon: m, CookieNameSuffix: cookieName, Request: req, }) } macaroon-bakery-3.0.2/httpbakery/oven_test.go000066400000000000000000000141111464427415500213120ustar00rootroot00000000000000package httpbakery_test import ( "context" "fmt" "io/ioutil" "net/http" "net/http/httptest" "testing" "time" qt "github.com/frankban/quicktest" "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/bakery/checkers" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/identchecker" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakerytest" "github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery" ) func TestOvenWithAuthnMacaroon(t *testing.T) { c := qt.New(t) discharger := newTestIdentityServer() defer discharger.Close() key, err := bakery.GenerateKey() if err != nil { panic(err) } b := identchecker.NewBakery(identchecker.BakeryParams{ Location: "here", Locator: discharger, Key: key, Checker: httpbakery.NewChecker(), IdentityClient: discharger, }) expectedExpiry := time.Hour oven := &httpbakery.Oven{ Oven: b.Oven, AuthnExpiry: expectedExpiry, AuthzExpiry: 5 * time.Minute, } errorCalled := 0 handler := httpReqServer.HandleErrors(func(p httprequest.Params) error { if _, err := b.Checker.Auth(httpbakery.RequestMacaroons(p.Request)...).Allow(p.Context, identchecker.LoginOp); err != nil { errorCalled++ return oven.Error(testContext, p.Request, err) } fmt.Fprintf(p.Response, "done") return nil }) ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { handler(w, req, nil) })) defer ts.Close() req, err := http.NewRequest("GET", ts.URL, nil) c.Assert(err, qt.Equals, nil) client := httpbakery.NewClient() t0 := time.Now() resp, err := client.Do(req) c.Assert(err, qt.Equals, nil) c.Check(errorCalled, qt.Equals, 1) body, _ := ioutil.ReadAll(resp.Body) c.Assert(resp.StatusCode, qt.Equals, http.StatusOK, qt.Commentf("body: %q", body)) mss := httpbakery.MacaroonsForURL(client.Jar, mustParseURL(discharger.Location())) c.Assert(mss, qt.HasLen, 1) t1, ok := checkers.MacaroonsExpiryTime(b.Checker.Namespace(), mss[0]) c.Assert(ok, qt.Equals, true) want := t0.Add(expectedExpiry) if t1.Before(want) || t1.After(want.Add(time.Second)) { c.Fatalf("time out of range; got %v want %v", t1, want) } } func TestOvenWithAuthzMacaroon(t *testing.T) { c := qt.New(t) discharger := newTestIdentityServer() defer discharger.Close() discharger2 := bakerytest.NewDischarger(nil) defer discharger2.Close() locator := httpbakery.NewThirdPartyLocator(nil, nil) locator.AllowInsecure() key, err := bakery.GenerateKey() if err != nil { panic(err) } b := identchecker.NewBakery(identchecker.BakeryParams{ Location: "here", Locator: locator, Key: key, Checker: httpbakery.NewChecker(), IdentityClient: discharger, Authorizer: identchecker.AuthorizerFunc(func(ctx context.Context, id identchecker.Identity, op bakery.Op) (bool, []checkers.Caveat, error) { if id == nil { return false, nil, nil } return true, []checkers.Caveat{{ Location: discharger2.Location(), Condition: "something", }}, nil }), }) expectedAuthnExpiry := 5 * time.Minute expectedAuthzExpiry := time.Hour oven := &httpbakery.Oven{ Oven: b.Oven, AuthnExpiry: expectedAuthnExpiry, AuthzExpiry: expectedAuthzExpiry, } errorCalled := 0 handler := httpReqServer.HandleErrors(func(p httprequest.Params) error { if _, err := b.Checker.Auth(httpbakery.RequestMacaroons(p.Request)...).Allow(p.Context, bakery.Op{"something", "read"}); err != nil { errorCalled++ return oven.Error(testContext, p.Request, err) } fmt.Fprintf(p.Response, "done") return nil }) ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { handler(w, req, nil) })) defer ts.Close() req, err := http.NewRequest("GET", ts.URL, nil) c.Assert(err, qt.Equals, nil) client := httpbakery.NewClient() t0 := time.Now() resp, err := client.Do(req) c.Assert(err, qt.Equals, nil) c.Check(errorCalled, qt.Equals, 2) body, _ := ioutil.ReadAll(resp.Body) c.Assert(resp.StatusCode, qt.Equals, http.StatusOK, qt.Commentf("body: %q", body)) cookies := client.Jar.Cookies(mustParseURL(discharger.Location())) for i, cookie := range cookies { c.Logf("cookie %d: %s %q", i, cookie.Name, cookie.Value) } mss := httpbakery.MacaroonsForURL(client.Jar, mustParseURL(discharger.Location())) c.Assert(mss, qt.HasLen, 2) // The cookie jar returns otherwise-similar cookies in the order // they were added, so the authn macaroon will be first. t1, ok := checkers.MacaroonsExpiryTime(b.Checker.Namespace(), mss[0]) c.Assert(ok, qt.Equals, true) want := t0.Add(expectedAuthnExpiry) if t1.Before(want) || t1.After(want.Add(time.Second)) { c.Fatalf("time out of range; got %v want %v", t1, want) } t1, ok = checkers.MacaroonsExpiryTime(b.Checker.Namespace(), mss[1]) c.Assert(ok, qt.Equals, true) want = t0.Add(expectedAuthzExpiry) if t1.Before(want) || t1.After(want.Add(time.Second)) { c.Fatalf("time out of range; got %v want %v", t1, want) } } type testIdentityServer struct { *bakerytest.Discharger } func newTestIdentityServer() *testIdentityServer { checker := func(ctx context.Context, p httpbakery.ThirdPartyCaveatCheckerParams) ([]checkers.Caveat, error) { if string(p.Caveat.Condition) != "is-authenticated-user" { return nil, errgo.New("unexpected caveat") } return []checkers.Caveat{ checkers.DeclaredCaveat("username", "bob"), }, nil } discharger := bakerytest.NewDischarger(nil) discharger.CheckerP = httpbakery.ThirdPartyCaveatCheckerPFunc(checker) return &testIdentityServer{ Discharger: discharger, } } func (s *testIdentityServer) IdentityFromContext(ctx context.Context) (identchecker.Identity, []checkers.Caveat, error) { return nil, []checkers.Caveat{{ Location: s.Location(), Condition: "is-authenticated-user", }}, nil } func (s *testIdentityServer) DeclaredIdentity(ctx context.Context, declared map[string]string) (identchecker.Identity, error) { username, ok := declared["username"] if !ok { return nil, errgo.New("no username declared") } return identchecker.SimpleIdentity(username), nil } macaroon-bakery-3.0.2/httpbakery/request.go000066400000000000000000000125411464427415500210010ustar00rootroot00000000000000package httpbakery import ( "bytes" "context" "io" "net/http" "reflect" "sync" "sync/atomic" "gopkg.in/errgo.v1" ) // newRetrableRequest wraps an HTTP request so that it can // be retried without incurring race conditions and reports // whether the request can be retried. // The client instance will be used to make the request // when the do method is called. // // Because http.NewRequest often wraps its request bodies // with ioutil.NopCloser, which hides whether the body is // seekable, we extract the seeker from inside the nopCloser if // possible. // // We also work around Go issue 12796 by preventing concurrent // reads to the underlying reader after the request body has // been closed by Client.Do. // // The returned value should be closed after use. func newRetryableRequest(client *http.Client, req *http.Request) (*retryableRequest, bool) { if req.Body == nil { return &retryableRequest{ client: client, ref: 1, req: req, origCookie: req.Header.Get("Cookie"), }, true } body := seekerFromBody(req.Body) if body == nil { return nil, false } return &retryableRequest{ client: client, ref: 1, req: req, body: body, origCookie: req.Header.Get("Cookie"), }, true } type retryableRequest struct { client *http.Client ref int32 origCookie string body readSeekCloser readStopper *readStopper req *http.Request } // do performs the HTTP request. func (rreq *retryableRequest) do(ctx context.Context) (*http.Response, error) { req, err := rreq.prepare() if err != nil { return nil, errgo.Mask(err) } return rreq.client.Do(req.WithContext(ctx)) } // prepare returns a new HTTP request object // by copying the original request and seeking // back to the start of the original body if needed. // // It needs to make a copy of the request because // the HTTP code can access the Request.Body field // after Client.Do has returned, which means we can't // replace it for the second request. func (rreq *retryableRequest) prepare() (*http.Request, error) { req := new(http.Request) *req = *rreq.req // Make sure that the original cookie header is still in place // so that we only end up with the cookies that are actually // added by the HTTP cookie logic, and not the ones that were // added in previous requests too. req.Header.Set("Cookie", rreq.origCookie) if rreq.body == nil { // No need for any of the seek shenanigans. return req, nil } if rreq.readStopper != nil { // We've made a previous request. Close its request // body so it can't interfere with the new request's body // and then seek back to the start. rreq.readStopper.Close() if _, err := rreq.body.Seek(0, 0); err != nil { return nil, errgo.Notef(err, "cannot seek to start of request body") } } atomic.AddInt32(&rreq.ref, 1) // Replace the request body with a new readStopper so that // we can stop a second request from interfering with current // request's body. rreq.readStopper = &readStopper{ rreq: rreq, r: rreq.body, } req.Body = rreq.readStopper return req, nil } // close closes the request. It closes the underlying reader // when all references have gone. func (req *retryableRequest) close() error { if atomic.AddInt32(&req.ref, -1) == 0 && req.body != nil { // We've closed it for the last time, so actually close // the original body. return req.body.Close() } return nil } // readStopper works around an issue with the net/http // package (see http://golang.org/issue/12796). // Because the first HTTP request might not have finished // reading from its body when it returns, we need to // ensure that the second request does not race on Read, // so this type implements a Reader that prevents all Read // calls to the underlying Reader after Close has been called. type readStopper struct { rreq *retryableRequest mu sync.Mutex r io.ReadSeeker } func (r *readStopper) Read(buf []byte) (int, error) { r.mu.Lock() defer r.mu.Unlock() if r.r == nil { // Note: we have to use io.EOF here because otherwise // another connection can in rare circumstances be // polluted by the error returned here. Although this // means the file may appear truncated to the server, // that shouldn't matter because the body will only // be closed after the server has replied. return 0, io.EOF } return r.r.Read(buf) } func (r *readStopper) Close() error { r.mu.Lock() alreadyClosed := r.r == nil r.r = nil r.mu.Unlock() if alreadyClosed { return nil } return r.rreq.close() } var nopCloserType = reflect.TypeOf(io.NopCloser(nil)) var nopCloserWriterToType = reflect.TypeOf(io.NopCloser(bytes.NewReader([]byte{}))) type readSeekCloser interface { io.ReadSeeker io.Closer } // seekerFromBody tries to obtain a seekable reader // from the given request body. func seekerFromBody(r io.ReadCloser) readSeekCloser { if r, ok := r.(readSeekCloser); ok { return r } rv := reflect.ValueOf(r) if rv.Type() != nopCloserType && rv.Type() != nopCloserWriterToType { return nil } // It's a value created by nopCloser. Extract the // underlying Reader. Note that this works // because the ioutil.nopCloser type exports // its Reader field. rs, ok := rv.Field(0).Interface().(io.ReadSeeker) if !ok { return nil } return readSeekerWithNopClose{rs} } type readSeekerWithNopClose struct { io.ReadSeeker } func (r readSeekerWithNopClose) Close() error { return nil } macaroon-bakery-3.0.2/httpbakery/visitor.go000066400000000000000000000043101464427415500210030ustar00rootroot00000000000000package httpbakery import ( "context" "net/http" "net/url" "gopkg.in/errgo.v1" "gopkg.in/httprequest.v1" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" ) // TODO(rog) rename this file. // legacyGetInteractionMethods queries a URL as found in an // ErrInteractionRequired VisitURL field to find available interaction // methods. // // It does this by sending a GET request to the URL with the Accept // header set to "application/json" and parsing the resulting // response as a map[string]string. // // It uses the given Doer to execute the HTTP GET request. func legacyGetInteractionMethods(ctx context.Context, logger bakery.Logger, client httprequest.Doer, u *url.URL) map[string]*url.URL { methodURLs, err := legacyGetInteractionMethods1(ctx, client, u) if err != nil { // When a discharger doesn't support retrieving interaction methods, // we expect to get an error, because it's probably returning an HTML // page not JSON. if logger != nil { logger.Debugf(ctx, "ignoring error: cannot get interaction methods: %v; %s", err, errgo.Details(err)) } methodURLs = make(map[string]*url.URL) } if methodURLs["interactive"] == nil { // There's no "interactive" method returned, but we know // the server does actually support it, because all dischargers // are required to, so fill it in with the original URL. methodURLs["interactive"] = u } return methodURLs } func legacyGetInteractionMethods1(ctx context.Context, client httprequest.Doer, u *url.URL) (map[string]*url.URL, error) { httpReqClient := &httprequest.Client{ Doer: client, } req, err := http.NewRequest("GET", u.String(), nil) if err != nil { return nil, errgo.Notef(err, "cannot create request") } req.Header.Set("Accept", "application/json") var methodURLStrs map[string]string if err := httpReqClient.Do(ctx, req, &methodURLStrs); err != nil { return nil, errgo.Mask(err) } // Make all the URLs relative to the request URL. methodURLs := make(map[string]*url.URL) for m, urlStr := range methodURLStrs { relURL, err := url.Parse(urlStr) if err != nil { return nil, errgo.Notef(err, "invalid URL for interaction method %q", m) } methodURLs[m] = u.ResolveReference(relURL) } return methodURLs, nil } macaroon-bakery-3.0.2/httpbakery/visitor_test.go000066400000000000000000000040711464427415500220460ustar00rootroot00000000000000package httpbakery_test import ( "context" "fmt" "net/http" "net/http/httptest" "net/url" "testing" qt "github.com/frankban/quicktest" "github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery" ) func TestLegacyGetInteractionMethodsGetFailure(t *testing.T) { c := qt.New(t) srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { w.WriteHeader(http.StatusTeapot) w.Write([]byte("failure")) })) defer srv.Close() methods := httpbakery.LegacyGetInteractionMethods(testContext, nopLogger{}, http.DefaultClient, mustParseURL(srv.URL)) // On error, it falls back to just the single default interactive method. c.Assert(methods, qt.DeepEquals, map[string]*url.URL{ "interactive": mustParseURL(srv.URL), }) } func TestLegacyGetInteractionMethodsSuccess(t *testing.T) { c := qt.New(t) srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { w.Header().Set("Content-Type", "application/json") fmt.Fprint(w, `{"method": "http://somewhere/something"}`) })) defer srv.Close() methods := httpbakery.LegacyGetInteractionMethods(testContext, nopLogger{}, http.DefaultClient, mustParseURL(srv.URL)) c.Assert(methods, qt.DeepEquals, map[string]*url.URL{ "interactive": mustParseURL(srv.URL), "method": mustParseURL("http://somewhere/something"), }) } func TestLegacyGetInteractionMethodsInvalidURL(t *testing.T) { c := qt.New(t) srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { w.Header().Set("Content-Type", "application/json") fmt.Fprint(w, `{"method": ":::"}`) })) defer srv.Close() methods := httpbakery.LegacyGetInteractionMethods(testContext, nopLogger{}, http.DefaultClient, mustParseURL(srv.URL)) // On error, it falls back to just the single default interactive method. c.Assert(methods, qt.DeepEquals, map[string]*url.URL{ "interactive": mustParseURL(srv.URL), }) } type nopLogger struct{} func (nopLogger) Debugf(context.Context, string, ...interface{}) {} func (nopLogger) Infof(context.Context, string, ...interface{}) {} macaroon-bakery-3.0.2/internal/000077500000000000000000000000001464427415500164165ustar00rootroot00000000000000macaroon-bakery-3.0.2/internal/httputil/000077500000000000000000000000001464427415500202735ustar00rootroot00000000000000macaroon-bakery-3.0.2/internal/httputil/relativeurl.go000066400000000000000000000037611464427415500231670ustar00rootroot00000000000000// Copyright 2016 Canonical Ltd. // Licensed under the LGPLv3, see LICENCE file for details. // Note: this code was copied from github.com/juju/utils. // Package httputil holds utility functions related to net/http. package httputil import ( "errors" "strings" ) // RelativeURLPath returns a relative URL path that is lexically // equivalent to targpath when interpreted by url.URL.ResolveReference. // On success, the returned path will always be non-empty and relative // to basePath, even if basePath and targPath share no elements. // // It is assumed that both basePath and targPath are normalized // (have no . or .. elements). // // An error is returned if basePath or targPath are not absolute paths. func RelativeURLPath(basePath, targPath string) (string, error) { if !strings.HasPrefix(basePath, "/") { return "", errors.New("non-absolute base URL") } if !strings.HasPrefix(targPath, "/") { return "", errors.New("non-absolute target URL") } baseParts := strings.Split(basePath, "/") targParts := strings.Split(targPath, "/") // For the purposes of dotdot, the last element of // the paths are irrelevant. We save the last part // of the target path for later. lastElem := targParts[len(targParts)-1] baseParts = baseParts[0 : len(baseParts)-1] targParts = targParts[0 : len(targParts)-1] // Find the common prefix between the two paths: var i int for ; i < len(baseParts); i++ { if i >= len(targParts) || baseParts[i] != targParts[i] { break } } dotdotCount := len(baseParts) - i targOnly := targParts[i:] result := make([]string, 0, dotdotCount+len(targOnly)+1) for i := 0; i < dotdotCount; i++ { result = append(result, "..") } result = append(result, targOnly...) result = append(result, lastElem) final := strings.Join(result, "/") if final == "" { // If the final result is empty, the last element must // have been empty, so the target was slash terminated // and there were no previous elements, so "." // is appropriate. final = "." } return final, nil } macaroon-bakery-3.0.2/internal/httputil/relativeurl_test.go000066400000000000000000000057121464427415500242240ustar00rootroot00000000000000// Copyright 2016 Canonical Ltd. // Licensed under the LGPLv3, see LICENCE file for details. // Note: this code was copied from github.com/juju/utils. package httputil_test import ( "net/url" "testing" qt "github.com/frankban/quicktest" "github.com/go-macaroon-bakery/macaroon-bakery/v3/internal/httputil" ) var relativeURLTests = []struct { base string target string expect string expectError string }{{ expectError: "non-absolute base URL", }, { base: "/foo", expectError: "non-absolute target URL", }, { base: "foo", expectError: "non-absolute base URL", }, { base: "/foo", target: "foo", expectError: "non-absolute target URL", }, { base: "/foo", target: "/bar", expect: "bar", }, { base: "/foo/", target: "/bar", expect: "../bar", }, { base: "/bar", target: "/foo/", expect: "foo/", }, { base: "/foo/", target: "/bar/", expect: "../bar/", }, { base: "/foo/bar", target: "/bar/", expect: "../bar/", }, { base: "/foo/bar/", target: "/bar/", expect: "../../bar/", }, { base: "/foo/bar/baz", target: "/foo/targ", expect: "../targ", }, { base: "/foo/bar/baz/frob", target: "/foo/bar/one/two/", expect: "../one/two/", }, { base: "/foo/bar/baz/", target: "/foo/targ", expect: "../../targ", }, { base: "/foo/bar/baz/frob/", target: "/foo/bar/one/two/", expect: "../../one/two/", }, { base: "/foo/bar", target: "/foot/bar", expect: "../foot/bar", }, { base: "/foo/bar/baz/frob", target: "/foo/bar", expect: "../../bar", }, { base: "/foo/bar/baz/frob/", target: "/foo/bar", expect: "../../../bar", }, { base: "/foo/bar/baz/frob/", target: "/foo/bar/", expect: "../../", }, { base: "/foo/bar/baz", target: "/foo/bar/other", expect: "other", }, { base: "/foo/bar/", target: "/foo/bar/", expect: ".", }, { base: "/foo/bar", target: "/foo/bar", expect: "bar", }, { base: "/foo/bar/", target: "/foo/bar/", expect: ".", }, { base: "/foo/bar", target: "/foo/", expect: ".", }, { base: "/foo", target: "/", expect: ".", }, { base: "/foo/", target: "/", expect: "../", }, { base: "/foo/bar", target: "/", expect: "../", }, { base: "/foo/bar/", target: "/", expect: "../../", }} func TestRelativeURL(t *testing.T) { c := qt.New(t) for i, test := range relativeURLTests { c.Logf("test %d: %q %q", i, test.base, test.target) // Sanity check the test itself. if test.expectError == "" { baseURL := &url.URL{Path: test.base} expectURL := &url.URL{Path: test.expect} targetURL := baseURL.ResolveReference(expectURL) c.Check(targetURL.Path, qt.Equals, test.target, qt.Commentf("resolve reference failure (%q + %q != %q)", test.base, test.expect, test.target)) } result, err := httputil.RelativeURLPath(test.base, test.target) if test.expectError != "" { c.Assert(err, qt.ErrorMatches, test.expectError) c.Assert(result, qt.Equals, "") } else { c.Assert(err, qt.IsNil) c.Check(result, qt.Equals, test.expect) } } }