pax_global_header 0000666 0000000 0000000 00000000064 14644274155 0014526 g ustar 00root root 0000000 0000000 52 comment=7e8973aec59c9b11e62dd3281069ef71f158cc0a
macaroon-bakery-3.0.2/ 0000775 0000000 0000000 00000000000 14644274155 0014602 5 ustar 00root root 0000000 0000000 macaroon-bakery-3.0.2/.github/ 0000775 0000000 0000000 00000000000 14644274155 0016142 5 ustar 00root root 0000000 0000000 macaroon-bakery-3.0.2/.github/dependabot.yaml 0000664 0000000 0000000 00000000477 14644274155 0021143 0 ustar 00root root 0000000 0000000 version: 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/ 0000775 0000000 0000000 00000000000 14644274155 0020177 5 ustar 00root root 0000000 0000000 macaroon-bakery-3.0.2/.github/workflows/ci.yaml 0000664 0000000 0000000 00000002343 14644274155 0021460 0 ustar 00root root 0000000 0000000 name: 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/.gitignore 0000664 0000000 0000000 00000000016 14644274155 0016567 0 ustar 00root root 0000000 0000000 *.test
/.idea/ macaroon-bakery-3.0.2/LICENSE 0000664 0000000 0000000 00000021133 14644274155 0015607 0 ustar 00root root 0000000 0000000 Copyright © 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.md 0000664 0000000 0000000 00000000647 14644274155 0016070 0 ustar 00root root 0000000 0000000 # 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/TODO 0000664 0000000 0000000 00000000224 14644274155 0015270 0 ustar 00root root 0000000 0000000 all:
- 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/ 0000775 0000000 0000000 00000000000 14644274155 0016057 5 ustar 00root root 0000000 0000000 macaroon-bakery-3.0.2/bakery/authstore_test.go 0000664 0000000 0000000 00000005106 14644274155 0021465 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000005553 14644274155 0017673 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000035176 14644274155 0020026 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000047630 14644274155 0021063 0 ustar 00root root 0000000 0000000 package 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/ 0000775 0000000 0000000 00000000000 14644274155 0017646 5 ustar 00root root 0000000 0000000 macaroon-bakery-3.0.2/bakery/checkers/checkers.go 0000664 0000000 0000000 00000016603 14644274155 0021772 0 ustar 00root root 0000000 0000000 // 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.go 0000664 0000000 0000000 00000032542 14644274155 0023031 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000010224 14644274155 0021737 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000014263 14644274155 0022137 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000017540 14644274155 0023177 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000004431 14644274155 0021135 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000007312 14644274155 0022175 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000027325 14644274155 0017474 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000013261 14644274155 0020525 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000006326 14644274155 0020744 0 ustar 00root root 0000000 0000000 package 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/ 0000775 0000000 0000000 00000000000 14644274155 0021136 5 ustar 00root root 0000000 0000000 macaroon-bakery-3.0.2/bakery/dbrootkeystore/rootkey.go 0000664 0000000 0000000 00000031342 14644274155 0023164 0 ustar 00root root 0000000 0000000 // 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.go 0000664 0000000 0000000 00000040057 14644274155 0024226 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000021567 14644274155 0020352 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000037632 14644274155 0021411 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000003706 14644274155 0021036 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000011611 14644274155 0022067 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000007147 14644274155 0017164 0 ustar 00root root 0000000 0000000 // 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.go 0000664 0000000 0000000 00000004463 14644274155 0017546 0 ustar 00root root 0000000 0000000 package 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/ 0000775 0000000 0000000 00000000000 14644274155 0017512 5 ustar 00root root 0000000 0000000 macaroon-bakery-3.0.2/bakery/example/authservice.go 0000664 0000000 0000000 00000002637 14644274155 0022373 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000001765 14644274155 0021330 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000003021 14644274155 0022527 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000004466 14644274155 0020777 0 ustar 00root root 0000000 0000000 // 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.go 0000664 0000000 0000000 00000006702 14644274155 0022715 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000000402 14644274155 0020762 0 ustar 00root root 0000000 0000000 package 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/ 0000775 0000000 0000000 00000000000 14644274155 0020507 5 ustar 00root root 0000000 0000000 macaroon-bakery-3.0.2/bakery/identchecker/authorizer.go 0000664 0000000 0000000 00000012103 14644274155 0023227 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000011257 14644274155 0024277 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000006021 14644274155 0022312 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000024776 14644274155 0022462 0 ustar 00root root 0000000 0000000 // 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.go 0000664 0000000 0000000 00000063017 14644274155 0023510 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000002764 14644274155 0023376 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000006044 14644274155 0022673 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000013042 14644274155 0017361 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000007763 14644274155 0020435 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000001370 14644274155 0017666 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000027112 14644274155 0020210 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000016735 14644274155 0021260 0 ustar 00root root 0000000 0000000 package 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/ 0000775 0000000 0000000 00000000000 14644274155 0021333 5 ustar 00root root 0000000 0000000 macaroon-bakery-3.0.2/bakery/mgorootkeystore/export_test.go 0000664 0000000 0000000 00000000153 14644274155 0024241 0 ustar 00root root 0000000 0000000 package mgorootkeystore
var (
Clock = &clock
MgoCollectionFindId = &mgoCollectionFindId
)
macaroon-bakery-3.0.2/bakery/mgorootkeystore/rootkey.go 0000664 0000000 0000000 00000015720 14644274155 0023363 0 ustar 00root root 0000000 0000000 // 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.go 0000664 0000000 0000000 00000037237 14644274155 0024431 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000025056 14644274155 0017365 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000005617 14644274155 0020425 0 ustar 00root root 0000000 0000000 package 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/ 0000775 0000000 0000000 00000000000 14644274155 0022417 5 ustar 00root root 0000000 0000000 macaroon-bakery-3.0.2/bakery/postgresrootkeystore/export_test.go 0000664 0000000 0000000 00000000365 14644274155 0025332 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000006712 14644274155 0024450 0 ustar 00root root 0000000 0000000 // 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.go 0000664 0000000 0000000 00000037231 14644274155 0025507 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000011651 14644274155 0023551 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000007567 14644274155 0017524 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000014555 14644274155 0020556 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000003211 14644274155 0017537 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000001603 14644274155 0020601 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000001434 14644274155 0020075 0 ustar 00root root 0000000 0000000 package 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/ 0000775 0000000 0000000 00000000000 14644274155 0016757 5 ustar 00root root 0000000 0000000 macaroon-bakery-3.0.2/bakerytest/bakerytest.go 0000664 0000000 0000000 00000015360 14644274155 0021470 0 ustar 00root root 0000000 0000000 // 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.go 0000664 0000000 0000000 00000032062 14644274155 0022525 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000011263 14644274155 0021515 0 ustar 00root root 0000000 0000000 package 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/ 0000775 0000000 0000000 00000000000 14644274155 0015345 5 ustar 00root root 0000000 0000000 macaroon-bakery-3.0.2/cmd/bakery-keygen/ 0000775 0000000 0000000 00000000000 14644274155 0020102 5 ustar 00root root 0000000 0000000 macaroon-bakery-3.0.2/cmd/bakery-keygen/go.mod 0000664 0000000 0000000 00000001136 14644274155 0021211 0 ustar 00root root 0000000 0000000 module 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.sum 0000664 0000000 0000000 00000023061 14644274155 0021237 0 ustar 00root root 0000000 0000000 cloud.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.go 0000664 0000000 0000000 00000000545 14644274155 0021361 0 ustar 00root root 0000000 0000000 package 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.mod 0000664 0000000 0000000 00000002605 14644274155 0015713 0 ustar 00root root 0000000 0000000 module 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.sum 0000664 0000000 0000000 00000035773 14644274155 0015754 0 ustar 00root root 0000000 0000000 cloud.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/ 0000775 0000000 0000000 00000000000 14644274155 0016757 5 ustar 00root root 0000000 0000000 macaroon-bakery-3.0.2/httpbakery/agent/ 0000775 0000000 0000000 00000000000 14644274155 0020055 5 ustar 00root root 0000000 0000000 macaroon-bakery-3.0.2/httpbakery/agent/agent.go 0000664 0000000 0000000 00000017044 14644274155 0021510 0 ustar 00root root 0000000 0000000 // 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.go 0000664 0000000 0000000 00000013305 14644274155 0022543 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000002745 14644274155 0021665 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000005567 14644274155 0022731 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000001155 14644274155 0023100 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000000111 14644274155 0022755 0 ustar 00root root 0000000 0000000 package agent
type AgentLogin agentLogin
const CookieName = cookieName
macaroon-bakery-3.0.2/httpbakery/agent/legacy_test.go 0000664 0000000 0000000 00000027774 14644274155 0022730 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000004303 14644274155 0022245 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000014636 14644274155 0021003 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000011025 14644274155 0021074 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000013136 14644274155 0022140 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000060111 14644274155 0020563 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000131415 14644274155 0021630 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000000235 14644274155 0021627 0 ustar 00root root 0000000 0000000 // +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.go 0000664 0000000 0000000 00000000245 14644274155 0022337 0 ustar 00root root 0000000 0000000 // +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.go 0000664 0000000 0000000 00000030254 14644274155 0021243 0 ustar 00root root 0000000 0000000 package httpbakery
import (
"context"
"encoding/base64"
"net/http"
"path"
"unicode/utf8"
"github.com/julienschmidt/httprouter"
"gopkg.in/errgo.v1"
"gopkg.in/httprequest.v1"
"gopkg.in/macaroon.v2"
"github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery"
"github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/checkers"
)
// ThirdPartyCaveatChecker is used to check third party caveats.
// This interface is deprecated and included only for backward
// compatibility; ThirdPartyCaveatCheckerP should be used instead.
type ThirdPartyCaveatChecker interface {
// CheckThirdPartyCaveat is like ThirdPartyCaveatCheckerP.CheckThirdPartyCaveat
// except that it uses separate arguments instead of a struct arg.
CheckThirdPartyCaveat(ctx context.Context, info *bakery.ThirdPartyCaveatInfo, req *http.Request, token *DischargeToken) ([]checkers.Caveat, error)
}
// ThirdPartyCaveatCheckerP is used to check third party caveats.
// The "P" stands for "Params" - this was added after ThirdPartyCaveatChecker
// which can't be removed without breaking backwards compatibility.
type ThirdPartyCaveatCheckerP interface {
// CheckThirdPartyCaveat is used to check whether a client
// making the given request should be allowed a discharge for
// the p.Info.Condition. On success, the caveat will be discharged,
// with any returned caveats also added to the discharge
// macaroon.
//
// The p.Token field, if non-nil, is a token obtained from
// Interactor.Interact as the result of a discharge interaction
// after an interaction required error.
//
// Note than when used in the context of a discharge handler
// created by Discharger, any returned errors will be marshaled
// as documented in DischargeHandler.ErrorMapper.
CheckThirdPartyCaveat(ctx context.Context, p ThirdPartyCaveatCheckerParams) ([]checkers.Caveat, error)
}
// ThirdPartyCaveatCheckerParams holds the parameters passed to
// CheckThirdPartyCaveatP.
type ThirdPartyCaveatCheckerParams struct {
// Caveat holds information about the caveat being discharged.
Caveat *bakery.ThirdPartyCaveatInfo
// Token holds the discharge token provided by the client, if any.
Token *DischargeToken
// Req holds the HTTP discharge request.
Request *http.Request
// Response holds the HTTP response writer. Implementations
// must not call its WriteHeader or Write methods.
Response http.ResponseWriter
}
// ThirdPartyCaveatCheckerFunc implements ThirdPartyCaveatChecker
// by calling a function.
type ThirdPartyCaveatCheckerFunc func(ctx context.Context, req *http.Request, info *bakery.ThirdPartyCaveatInfo, token *DischargeToken) ([]checkers.Caveat, error)
func (f ThirdPartyCaveatCheckerFunc) CheckThirdPartyCaveat(ctx context.Context, info *bakery.ThirdPartyCaveatInfo, req *http.Request, token *DischargeToken) ([]checkers.Caveat, error) {
return f(ctx, req, info, token)
}
// ThirdPartyCaveatCheckerPFunc implements ThirdPartyCaveatCheckerP
// by calling a function.
type ThirdPartyCaveatCheckerPFunc func(ctx context.Context, p ThirdPartyCaveatCheckerParams) ([]checkers.Caveat, error)
func (f ThirdPartyCaveatCheckerPFunc) CheckThirdPartyCaveat(ctx context.Context, p ThirdPartyCaveatCheckerParams) ([]checkers.Caveat, error) {
return f(ctx, p)
}
// newDischargeClient returns a discharge client that addresses the
// third party discharger at the given location URL and uses
// the given client to make HTTP requests.
//
// If client is nil, http.DefaultClient is used.
func newDischargeClient(location string, client httprequest.Doer) *dischargeClient {
if client == nil {
client = http.DefaultClient
}
return &dischargeClient{
Client: httprequest.Client{
BaseURL: location,
Doer: client,
UnmarshalError: unmarshalError,
},
}
}
// Discharger holds parameters for creating a new Discharger.
type DischargerParams struct {
// CheckerP is used to actually check the caveats.
// This will be used in preference to Checker.
CheckerP ThirdPartyCaveatCheckerP
// Checker is used to actually check the caveats.
// This should be considered deprecated and will be ignored if CheckerP is set.
Checker ThirdPartyCaveatChecker
// Key holds the key pair of the discharger.
Key *bakery.KeyPair
// Locator is used to find public keys when adding
// third-party caveats on discharge macaroons.
// If this is nil, no third party caveats may be added.
Locator bakery.ThirdPartyLocator
// ErrorToResponse is used to convert errors returned by the third
// party caveat checker to the form that will be JSON-marshaled
// on the wire. If zero, this defaults to ErrorToResponse.
// If set, it should handle errors that it does not understand
// by falling back to calling ErrorToResponse to ensure
// that the standard bakery errors are marshaled in the expected way.
ErrorToResponse func(ctx context.Context, err error) (int, interface{})
}
// Discharger represents a third-party caveat discharger.
// can discharge caveats in an HTTP server.
//
// The name space served by dischargers is as follows.
// All parameters can be provided either as URL attributes
// or form attributes. The result is always formatted as a JSON
// object.
//
// On failure, all endpoints return an error described by
// the Error type.
//
// POST /discharge
// params:
// id: all-UTF-8 third party caveat id
// id64: non-padded URL-base64 encoded caveat id
// macaroon-id: (optional) id to give to discharge macaroon (defaults to id)
// token: (optional) value of discharge token
// token64: (optional) base64-encoded value of discharge token.
// token-kind: (mandatory if token or token64 provided) discharge token kind.
// result on success (http.StatusOK):
// {
// Macaroon *macaroon.Macaroon
// }
//
// GET /publickey
// result:
// public key of service
// expiry time of key
type Discharger struct {
p DischargerParams
}
// NewDischarger returns a new third-party caveat discharger
// using the given parameters.
func NewDischarger(p DischargerParams) *Discharger {
if p.ErrorToResponse == nil {
p.ErrorToResponse = ErrorToResponse
}
if p.Locator == nil {
p.Locator = emptyLocator{}
}
if p.CheckerP == nil {
p.CheckerP = ThirdPartyCaveatCheckerPFunc(func(ctx context.Context, cp ThirdPartyCaveatCheckerParams) ([]checkers.Caveat, error) {
return p.Checker.CheckThirdPartyCaveat(ctx, cp.Caveat, cp.Request, cp.Token)
})
}
return &Discharger{
p: p,
}
}
type emptyLocator struct{}
func (emptyLocator) ThirdPartyInfo(ctx context.Context, loc string) (bakery.ThirdPartyInfo, error) {
return bakery.ThirdPartyInfo{}, bakery.ErrNotFound
}
// AddMuxHandlers adds handlers to the given ServeMux to provide
// a third-party caveat discharge service.
func (d *Discharger) AddMuxHandlers(mux *http.ServeMux, rootPath string) {
for _, h := range d.Handlers() {
// Note: this only works because we don't have any wildcard
// patterns in the discharger paths.
mux.Handle(path.Join(rootPath, h.Path), mkHTTPHandler(h.Handle))
}
}
// Handlers returns a slice of handlers that can handle a third-party
// caveat discharge service when added to an httprouter.Router.
// TODO provide some way of customizing the context so that
// ErrorToResponse can see a request-specific context.
func (d *Discharger) Handlers() []httprequest.Handler {
f := func(p httprequest.Params) (dischargeHandler, context.Context, error) {
return dischargeHandler{
discharger: d,
}, p.Context, nil
}
srv := httprequest.Server{
ErrorMapper: d.p.ErrorToResponse,
}
return srv.Handlers(f)
}
//go:generate httprequest-generate-client github.com/go-macaroon-bakery/macaroon-bakery/v3-unstable/httpbakery dischargeHandler dischargeClient
// dischargeHandler is the type used to define the httprequest handler
// methods for a discharger.
type dischargeHandler struct {
discharger *Discharger
}
// dischargeRequest is a request to create a macaroon that discharges the
// supplied third-party caveat. Discharging caveats will normally be
// handled by the bakery it would be unusual to use this type directly in
// client software.
type dischargeRequest struct {
httprequest.Route `httprequest:"POST /discharge"`
Id string `httprequest:"id,form,omitempty"`
Id64 string `httprequest:"id64,form,omitempty"`
Caveat string `httprequest:"caveat64,form,omitempty"`
Token string `httprequest:"token,form,omitempty"`
Token64 string `httprequest:"token64,form,omitempty"`
TokenKind string `httprequest:"token-kind,form,omitempty"`
}
// dischargeResponse contains the response from a /discharge POST request.
type dischargeResponse struct {
Macaroon *bakery.Macaroon `json:",omitempty"`
}
// Discharge discharges a third party caveat.
func (h dischargeHandler) Discharge(p httprequest.Params, r *dischargeRequest) (*dischargeResponse, error) {
id, err := maybeBase64Decode(r.Id, r.Id64)
if err != nil {
return nil, errgo.Notef(err, "bad caveat id")
}
var caveat []byte
if r.Caveat != "" {
// Note that it's important that when r.Caveat is empty,
// we leave DischargeParams.Caveat as nil (Base64Decode
// always returns a non-nil byte slice).
caveat1, err := macaroon.Base64Decode([]byte(r.Caveat))
if err != nil {
return nil, errgo.Notef(err, "bad base64-encoded caveat: %v", err)
}
caveat = caveat1
}
tokenVal, err := maybeBase64Decode(r.Token, r.Token64)
if err != nil {
return nil, errgo.Notef(err, "bad discharge token")
}
var token *DischargeToken
if len(tokenVal) != 0 {
if r.TokenKind == "" {
return nil, errgo.Notef(err, "discharge token provided without token kind")
}
token = &DischargeToken{
Kind: r.TokenKind,
Value: tokenVal,
}
}
m, err := bakery.Discharge(p.Context, bakery.DischargeParams{
Id: id,
Caveat: caveat,
Key: h.discharger.p.Key,
Checker: bakery.ThirdPartyCaveatCheckerFunc(
func(ctx context.Context, cav *bakery.ThirdPartyCaveatInfo) ([]checkers.Caveat, error) {
return h.discharger.p.CheckerP.CheckThirdPartyCaveat(ctx, ThirdPartyCaveatCheckerParams{
Caveat: cav,
Request: p.Request,
Response: p.Response,
Token: token,
})
},
),
Locator: h.discharger.p.Locator,
})
if err != nil {
return nil, errgo.NoteMask(err, "cannot discharge", errgo.Any)
}
return &dischargeResponse{m}, nil
}
// publicKeyRequest specifies the /publickey endpoint.
type publicKeyRequest struct {
httprequest.Route `httprequest:"GET /publickey"`
}
// publicKeyResponse is the response to a /publickey GET request.
type publicKeyResponse struct {
PublicKey *bakery.PublicKey
}
// dischargeInfoRequest specifies the /discharge/info endpoint.
type dischargeInfoRequest struct {
httprequest.Route `httprequest:"GET /discharge/info"`
}
// dischargeInfoResponse is the response to a /discharge/info GET
// request.
type dischargeInfoResponse struct {
PublicKey *bakery.PublicKey
Version bakery.Version
}
// PublicKey returns the public key of the discharge service.
func (h dischargeHandler) PublicKey(*publicKeyRequest) (publicKeyResponse, error) {
return publicKeyResponse{
PublicKey: &h.discharger.p.Key.Public,
}, nil
}
// DischargeInfo returns information on the discharger.
func (h dischargeHandler) DischargeInfo(*dischargeInfoRequest) (dischargeInfoResponse, error) {
return dischargeInfoResponse{
PublicKey: &h.discharger.p.Key.Public,
Version: bakery.LatestVersion,
}, nil
}
// mkHTTPHandler converts an httprouter handler to an http.Handler,
// assuming that the httprouter handler has no wildcard path
// parameters.
func mkHTTPHandler(h httprouter.Handle) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
h(w, req, nil)
})
}
// maybeBase64Encode encodes b as is if it's
// OK to be passed as a URL form parameter,
// or encoded as base64 otherwise.
func maybeBase64Encode(b []byte) (s, s64 string) {
if utf8.Valid(b) {
valid := true
for _, c := range b {
if c < 32 || c == 127 {
valid = false
break
}
}
if valid {
return string(b), ""
}
}
return "", base64.RawURLEncoding.EncodeToString(b)
}
// maybeBase64Decode implements the inverse of maybeBase64Encode.
func maybeBase64Decode(s, s64 string) ([]byte, error) {
if s64 != "" {
data, err := macaroon.Base64Decode([]byte(s64))
if err != nil {
return nil, errgo.Mask(err)
}
if len(data) == 0 {
return nil, nil
}
return data, nil
}
return []byte(s), nil
}
macaroon-bakery-3.0.2/httpbakery/dischargeclient_generated.go 0000664 0000000 0000000 00000001715 14644274155 0024460 0 ustar 00root root 0000000 0000000 // 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.go 0000664 0000000 0000000 00000026412 14644274155 0020444 0 ustar 00root root 0000000 0000000 package httpbakery
import (
"context"
"encoding/json"
"fmt"
"net/http"
"strconv"
"gopkg.in/errgo.v1"
"gopkg.in/httprequest.v1"
"github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery"
"github.com/go-macaroon-bakery/macaroon-bakery/v3/internal/httputil"
)
// ErrorCode holds an error code that classifies
// an error returned from a bakery HTTP handler.
type ErrorCode string
func (e ErrorCode) Error() string {
return string(e)
}
func (e ErrorCode) ErrorCode() ErrorCode {
return e
}
const (
ErrBadRequest = ErrorCode("bad request")
ErrDischargeRequired = ErrorCode("macaroon discharge required")
ErrInteractionRequired = ErrorCode("interaction required")
ErrInteractionMethodNotFound = ErrorCode("discharger does not provide an supported interaction method")
ErrPermissionDenied = ErrorCode("permission denied")
)
var httpReqServer = httprequest.Server{
ErrorMapper: ErrorToResponse,
}
// WriteError writes the given bakery error to w.
func WriteError(ctx context.Context, w http.ResponseWriter, err error) {
httpReqServer.WriteError(ctx, w, err)
}
// Error holds the type of a response from an httpbakery HTTP request,
// marshaled as JSON.
//
// Note: Do not construct Error values with ErrDischargeRequired or
// ErrInteractionRequired codes directly - use the
// NewDischargeRequiredError or NewInteractionRequiredError
// functions instead.
type Error struct {
Code ErrorCode `json:",omitempty"`
Message string `json:",omitempty"`
Info *ErrorInfo `json:",omitempty"`
// version holds the protocol version that was used
// to create the error (see NewDischargeRequiredError).
version bakery.Version
}
// ErrorInfo holds additional information provided
// by an error.
type ErrorInfo struct {
// Macaroon may hold a macaroon that, when
// discharged, may allow access to a service.
// This field is associated with the ErrDischargeRequired
// error code.
Macaroon *bakery.Macaroon `json:",omitempty"`
// MacaroonPath holds the URL path to be associated
// with the macaroon. The macaroon is potentially
// valid for all URLs under the given path.
// If it is empty, the macaroon will be associated with
// the original URL from which the error was returned.
MacaroonPath string `json:",omitempty"`
// CookieNameSuffix holds the desired cookie name suffix to be
// associated with the macaroon. The actual name used will be
// ("macaroon-" + CookieName). Clients may ignore this field -
// older clients will always use ("macaroon-" +
// macaroon.Signature() in hex).
CookieNameSuffix string `json:",omitempty"`
// The following fields are associated with the
// ErrInteractionRequired error code.
// InteractionMethods holds the set of methods that the
// third party supports for completing the discharge.
// See InteractionMethod for a more convenient
// accessor method.
InteractionMethods map[string]*json.RawMessage `json:",omitempty"`
// LegacyVisitURL holds a URL that the client should visit
// in a web browser to authenticate themselves.
// This is deprecated - it is superceded by the InteractionMethods
// field.
LegacyVisitURL string `json:"VisitURL,omitempty"`
// LegacyWaitURL holds a URL that the client should visit
// to acquire the discharge macaroon. A GET on
// this URL will block until the client has authenticated,
// and then it will return the discharge macaroon.
// This is deprecated - it is superceded by the InteractionMethods
// field.
LegacyWaitURL string `json:"WaitURL,omitempty"`
}
// SetInteraction sets the information for a particular
// interaction kind to v. The error should be an interaction-required
// error. This method will panic if v cannot be JSON-marshaled.
// It is expected that interaction implementations will
// implement type-safe wrappers for this method,
// so you should not need to call it directly.
func (e *Error) SetInteraction(kind string, v interface{}) {
if e.Info == nil {
e.Info = new(ErrorInfo)
}
if e.Info.InteractionMethods == nil {
e.Info.InteractionMethods = make(map[string]*json.RawMessage)
}
data, err := json.Marshal(v)
if err != nil {
panic(err)
}
m := json.RawMessage(data)
e.Info.InteractionMethods[kind] = &m
}
// InteractionMethod checks whether the error is an InteractionRequired error
// that implements the method with the given name, and JSON-unmarshals the
// method-specific data into x.
func (e *Error) InteractionMethod(kind string, x interface{}) error {
if e.Info == nil || e.Code != ErrInteractionRequired {
return errgo.Newf("not an interaction-required error (code %v)", e.Code)
}
entry := e.Info.InteractionMethods[kind]
if entry == nil {
return errgo.WithCausef(nil, ErrInteractionMethodNotFound, "interaction method %q not found", kind)
}
if err := json.Unmarshal(*entry, x); err != nil {
return errgo.Notef(err, "cannot unmarshal data for interaction method %q", kind)
}
return nil
}
func (e *Error) Error() string {
return e.Message
}
func (e *Error) ErrorCode() ErrorCode {
return e.Code
}
// ErrorInfo returns additional information
// about the error.
// TODO return interface{} here?
func (e *Error) ErrorInfo() *ErrorInfo {
return e.Info
}
// ErrorToResponse returns the HTTP status and an error body to be
// marshaled as JSON for the given error. This allows a third party
// package to integrate bakery errors into their error responses when
// they encounter an error with a *bakery.Error cause.
func ErrorToResponse(ctx context.Context, err error) (int, interface{}) {
errorBody := errorResponseBody(err)
var body interface{} = errorBody
status := http.StatusInternalServerError
switch errorBody.Code {
case ErrBadRequest:
status = http.StatusBadRequest
case ErrPermissionDenied:
status = http.StatusUnauthorized
case ErrDischargeRequired, ErrInteractionRequired:
switch errorBody.version {
case bakery.Version0:
status = http.StatusProxyAuthRequired
case bakery.Version1, bakery.Version2, bakery.Version3:
status = http.StatusUnauthorized
body = httprequest.CustomHeader{
Body: body,
SetHeaderFunc: setAuthenticateHeader,
}
default:
panic(fmt.Sprintf("out of range version number %v", errorBody.version))
}
}
return status, body
}
func setAuthenticateHeader(h http.Header) {
h.Set("WWW-Authenticate", "Macaroon")
}
type errorInfoer interface {
ErrorInfo() *ErrorInfo
}
type errorCoder interface {
ErrorCode() ErrorCode
}
// errorResponse returns an appropriate error
// response for the provided error.
func errorResponseBody(err error) *Error {
var errResp Error
cause := errgo.Cause(err)
if cause, ok := cause.(*Error); ok {
// It's an Error already. Preserve the wrapped
// error message but copy everything else.
errResp = *cause
errResp.Message = err.Error()
return &errResp
}
// It's not an error. Preserve as much info as
// we can find.
errResp.Message = err.Error()
if coder, ok := cause.(errorCoder); ok {
errResp.Code = coder.ErrorCode()
}
if infoer, ok := cause.(errorInfoer); ok {
errResp.Info = infoer.ErrorInfo()
}
return &errResp
}
// NewInteractionRequiredError returns an error of type *Error
// that requests an interaction from the client in response
// to the given request. The originalErr value describes the original
// error - if it is nil, a default message will be provided.
//
// This function should be used in preference to creating the Error value
// directly, as it sets the bakery protocol version correctly in the error.
//
// The returned error does not support any interaction kinds.
// Use kind-specific SetInteraction methods (for example
// WebBrowserInteractor.SetInteraction) to add supported
// interaction kinds.
//
// Note that WebBrowserInteractor.SetInteraction should always be called
// for legacy clients to maintain backwards compatibility.
func NewInteractionRequiredError(originalErr error, req *http.Request) *Error {
if originalErr == nil {
originalErr = ErrInteractionRequired
}
return &Error{
Message: originalErr.Error(),
version: RequestVersion(req),
Code: ErrInteractionRequired,
}
}
type DischargeRequiredErrorParams struct {
// Macaroon holds the macaroon that needs to be discharged
// by the client.
Macaroon *bakery.Macaroon
// OriginalError holds the reason that the discharge-required
// error was created. If it's nil, ErrDischargeRequired will
// be used.
OriginalError error
// CookiePath holds the path for the client to give the cookie
// holding the discharged macaroon. If it's empty, then a
// relative path from the request URL path to / will be used if
// Request is provided, or "/" otherwise.
CookiePath string
// CookieNameSuffix holds the suffix for the client
// to give the cookie holding the discharged macaroon
// (after the "macaroon-" prefix).
// If it's empty, "auth" will be used.
CookieNameSuffix string
// Request holds the request that the error is in response to.
// It is used to form the cookie path if CookiePath is empty.
Request *http.Request
}
// NewDischargeRequiredErrorWithVersion returns an error of type *Error
// that contains a macaroon to the client and acts as a request that the
// macaroon be discharged to authorize the request.
//
// The client is responsible for discharging the macaroon and
// storing it as a cookie (or including it as a Macaroon header)
// to be used for the subsequent request.
func NewDischargeRequiredError(p DischargeRequiredErrorParams) error {
if p.OriginalError == nil {
p.OriginalError = ErrDischargeRequired
}
if p.CookiePath == "" {
p.CookiePath = "/"
if p.Request != nil {
path, err := httputil.RelativeURLPath(p.Request.URL.Path, "/")
if err == nil {
p.CookiePath = path
}
}
}
if p.CookieNameSuffix == "" {
p.CookieNameSuffix = "auth"
}
return &Error{
version: p.Macaroon.Version(),
Message: p.OriginalError.Error(),
Code: ErrDischargeRequired,
Info: &ErrorInfo{
Macaroon: p.Macaroon,
MacaroonPath: p.CookiePath,
CookieNameSuffix: p.CookieNameSuffix,
},
}
}
// BakeryProtocolHeader is the header that HTTP clients should set
// to determine the bakery protocol version. If it is 0 or missing,
// a discharge-required error response will be returned with HTTP status 407;
// if it is 1, the response will have status 401 with the WWW-Authenticate
// header set to "Macaroon".
const BakeryProtocolHeader = "Bakery-Protocol-Version"
// RequestVersion determines the bakery protocol version from a client
// request. If the protocol cannot be determined, or is invalid, the
// original version of the protocol is used. If a later version is
// found, the latest known version is used, which is OK because versions
// are backwardly compatible.
//
// TODO as there are no known version 0 clients, default to version 1
// instead.
func RequestVersion(req *http.Request) bakery.Version {
vs := req.Header.Get(BakeryProtocolHeader)
if vs == "" {
// No header - use backward compatibility mode.
return bakery.Version0
}
x, err := strconv.Atoi(vs)
if err != nil || x < 0 {
// Badly formed header - use backward compatibility mode.
return bakery.Version0
}
v := bakery.Version(x)
if v > bakery.LatestVersion {
// Later version than we know about - use the
// latest version that we can.
return bakery.LatestVersion
}
return v
}
func isDischargeRequiredError(err error) bool {
respErr, ok := errgo.Cause(err).(*Error)
if !ok {
return false
}
return respErr.Code == ErrDischargeRequired
}
macaroon-bakery-3.0.2/httpbakery/error_test.go 0000664 0000000 0000000 00000016446 14644274155 0021511 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000000255 14644274155 0021670 0 ustar 00root root 0000000 0000000 package httpbakery
type PublicKeyResponse publicKeyResponse
const MaxDischargeRetries = maxDischargeRetries
var LegacyGetInteractionMethods = legacyGetInteractionMethods
macaroon-bakery-3.0.2/httpbakery/form/ 0000775 0000000 0000000 00000000000 14644274155 0017722 5 ustar 00root root 0000000 0000000 macaroon-bakery-3.0.2/httpbakery/form/form.go 0000664 0000000 0000000 00000012325 14644274155 0021217 0 ustar 00root root 0000000 0000000 // 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.go 0000664 0000000 0000000 00000021426 14644274155 0022260 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000007247 14644274155 0020770 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000015450 14644274155 0022022 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000005625 14644274155 0020265 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000014111 14644274155 0021312 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000012541 14644274155 0021001 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000004310 14644274155 0021003 0 ustar 00root root 0000000 0000000 package 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.go 0000664 0000000 0000000 00000004071 14644274155 0022046 0 ustar 00root root 0000000 0000000 package 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/ 0000775 0000000 0000000 00000000000 14644274155 0016416 5 ustar 00root root 0000000 0000000 macaroon-bakery-3.0.2/internal/httputil/ 0000775 0000000 0000000 00000000000 14644274155 0020273 5 ustar 00root root 0000000 0000000 macaroon-bakery-3.0.2/internal/httputil/relativeurl.go 0000664 0000000 0000000 00000003761 14644274155 0023167 0 ustar 00root root 0000000 0000000 // 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.go 0000664 0000000 0000000 00000005712 14644274155 0024224 0 ustar 00root root 0000000 0000000 // 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)
}
}
}