├── .github ├── dependabot.yaml └── workflows │ └── ci.yaml ├── .gitignore ├── LICENSE ├── README.md ├── TODO ├── bakery ├── authstore_test.go ├── bakery.go ├── checker.go ├── checker_test.go ├── checkers │ ├── checkers.go │ ├── checkers_test.go │ ├── declared.go │ ├── namespace.go │ ├── namespace_test.go │ ├── time.go │ └── time_test.go ├── codec.go ├── codec_test.go ├── common_test.go ├── dbrootkeystore │ ├── rootkey.go │ └── rootkey_test.go ├── discharge.go ├── discharge_test.go ├── dischargeall.go ├── dischargeall_test.go ├── doc.go ├── error.go ├── example │ ├── authservice.go │ ├── client.go │ ├── example_test.go │ ├── main.go │ └── targetservice.go ├── export_test.go ├── identchecker │ ├── authorizer.go │ ├── authorizer_test.go │ ├── bakery.go │ ├── checker.go │ ├── checker_test.go │ ├── common_test.go │ └── identity.go ├── keys.go ├── keys_test.go ├── logger.go ├── macaroon.go ├── macaroon_test.go ├── mgorootkeystore │ ├── export_test.go │ ├── rootkey.go │ └── rootkey_test.go ├── oven.go ├── oven_test.go ├── postgresrootkeystore │ ├── export_test.go │ ├── rootkey.go │ ├── rootkey_test.go │ └── sql.go ├── slice.go ├── slice_test.go ├── store.go ├── store_test.go └── version.go ├── bakerytest ├── bakerytest.go ├── bakerytest_test.go └── rendezvous.go ├── cmd └── bakery-keygen │ ├── go.mod │ ├── go.sum │ └── main.go ├── go.mod ├── go.sum ├── httpbakery ├── agent │ ├── agent.go │ ├── agent_test.go │ ├── cookie.go │ ├── cookie_test.go │ ├── example_test.go │ ├── export_test.go │ ├── legacy_test.go │ └── protocol.go ├── browser.go ├── checkers.go ├── checkers_test.go ├── client.go ├── client_test.go ├── context_go17.go ├── context_prego17.go ├── discharge.go ├── dischargeclient_generated.go ├── error.go ├── error_test.go ├── export_test.go ├── form │ ├── form.go │ └── form_test.go ├── keyring.go ├── keyring_test.go ├── oven.go ├── oven_test.go ├── request.go ├── visitor.go └── visitor_test.go └── internal └── httputil ├── relativeurl.go └── relativeurl_test.go /.github/dependabot.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | # Check for updates to GitHub Actions every weekday 8 | interval: "daily" 9 | 10 | - package-ecosystem: "gomod" 11 | directory: "/" 12 | schedule: 13 | # Check for updates to go modules every weekday 14 | interval: "daily" 15 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | build_test: 6 | name: Build and Test 7 | strategy: 8 | matrix: 9 | go: ['1.17','1.18','1.19'] 10 | runs-on: ubuntu-latest 11 | container: 12 | image: ubuntu 13 | volumes: 14 | - /etc/ssl/certs:/etc/ssl/certs 15 | services: 16 | postgres: 17 | image: ubuntu/postgres 18 | env: 19 | POSTGRES_PASSWORD: password 20 | # Set health checks to wait until postgres has started 21 | options: >- 22 | --health-cmd pg_isready 23 | --health-interval 10s 24 | --health-timeout 5s 25 | --health-retries 5 26 | mongo: 27 | image: mongo:4.4-bionic 28 | steps: 29 | - uses: actions/checkout@v2.3.4 30 | - uses: actions/setup-go@v2.1.3 31 | with: 32 | go-version: ${{ matrix.go }} 33 | stable: false 34 | - uses: actions/cache@v2.1.4 35 | with: 36 | path: ~/go/pkg/mod 37 | key: ubuntu-go-${{ hashFiles('**/go.sum') }} 38 | restore-keys: | 39 | ubuntu-go- 40 | - name: Install dependencies 41 | run: apt-get update -y && apt-get install -y gcc git-core 42 | - name: Build and Test 43 | run: go test ./... 44 | env: 45 | MGOCONNECTIONSTRING: mongo 46 | PGHOST: postgres 47 | PGPASSWORD: password 48 | PGSSLMODE: disable 49 | PGUSER: postgres 50 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.test 2 | /.idea/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # The macaroon bakery 2 | 3 | This repository is a companion to http://github.com/go-macaroon . 4 | It holds higher level operations for building systems with macaroons. 5 | 6 | For documentation, see: 7 | 8 | - http://godoc.org/github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery 9 | - http://godoc.org/github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery 10 | - http://godoc.org/github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/checkers 11 | -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | all: 2 | - when API is stable, move to gopkg.in/macaroon.v1 3 | 4 | macaroon: 5 | 6 | - change all signature calculations to correspond exactly 7 | with libmacaroons. 8 | -------------------------------------------------------------------------------- /bakery/authstore_test.go: -------------------------------------------------------------------------------- 1 | package bakery_test 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | 7 | "gopkg.in/errgo.v1" 8 | "gopkg.in/macaroon.v2" 9 | 10 | "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" 11 | "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/checkers" 12 | ) 13 | 14 | type macaroonStore struct { 15 | rootKeyStore bakery.RootKeyStore 16 | 17 | key *bakery.KeyPair 18 | 19 | locator bakery.ThirdPartyLocator 20 | } 21 | 22 | // newMacaroonStore returns a MacaroonVerifier implementation 23 | // that stores root keys in memory and puts all operations 24 | // in the macaroon id. 25 | func newMacaroonStore(locator bakery.ThirdPartyLocator) *macaroonStore { 26 | return &macaroonStore{ 27 | rootKeyStore: bakery.NewMemRootKeyStore(), 28 | key: mustGenerateKey(), 29 | locator: locator, 30 | } 31 | } 32 | 33 | type macaroonId struct { 34 | Id []byte 35 | Ops []bakery.Op 36 | } 37 | 38 | func (s *macaroonStore) NewMacaroon(ctx context.Context, ops []bakery.Op, caveats []checkers.Caveat, ns *checkers.Namespace) (*bakery.Macaroon, error) { 39 | rootKey, id, err := s.rootKeyStore.RootKey(ctx) 40 | if err != nil { 41 | return nil, errgo.Mask(err) 42 | } 43 | 44 | mid := macaroonId{ 45 | Id: id, 46 | Ops: ops, 47 | } 48 | data, _ := json.Marshal(mid) 49 | m, err := bakery.NewMacaroon(rootKey, data, "", bakery.LatestVersion, ns) 50 | if err != nil { 51 | return nil, errgo.Mask(err) 52 | } 53 | if err := m.AddCaveats(ctx, caveats, s.key, s.locator); err != nil { 54 | return nil, errgo.Mask(err) 55 | } 56 | return m, nil 57 | } 58 | 59 | func (s *macaroonStore) VerifyMacaroon(ctx context.Context, ms macaroon.Slice) (ops []bakery.Op, conditions []string, err error) { 60 | if len(ms) == 0 { 61 | return nil, nil, &bakery.VerificationError{ 62 | Reason: errgo.Newf("no macaroons in slice"), 63 | } 64 | } 65 | id := ms[0].Id() 66 | var mid macaroonId 67 | if err := json.Unmarshal(id, &mid); err != nil { 68 | return nil, nil, &bakery.VerificationError{ 69 | Reason: errgo.Notef(err, "bad macaroon id"), 70 | } 71 | } 72 | rootKey, err := s.rootKeyStore.Get(ctx, mid.Id) 73 | if err != nil { 74 | if errgo.Cause(err) == bakery.ErrNotFound { 75 | return nil, nil, &bakery.VerificationError{ 76 | Reason: errgo.Notef(err, "cannot find root key"), 77 | } 78 | } 79 | return nil, nil, errgo.Notef(err, "cannot find root key") 80 | } 81 | conditions, err = ms[0].VerifySignature(rootKey, ms[1:]) 82 | if err != nil { 83 | return nil, nil, &bakery.VerificationError{ 84 | Reason: errgo.Mask(err), 85 | } 86 | } 87 | return mid.Ops, conditions, nil 88 | } 89 | 90 | // macaroonVerifierWithError is an implementation of MacaroonVerifier that 91 | // returns the given error on all store operations. 92 | type macaroonVerifierWithError struct { 93 | err error 94 | } 95 | 96 | func (s macaroonVerifierWithError) VerifyMacaroon(ctx context.Context, ms macaroon.Slice) (ops []bakery.Op, conditions []string, err error) { 97 | return nil, nil, errgo.Mask(s.err, errgo.Any) 98 | } 99 | -------------------------------------------------------------------------------- /bakery/bakery.go: -------------------------------------------------------------------------------- 1 | package bakery 2 | 3 | import ( 4 | "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/checkers" 5 | ) 6 | 7 | // Bakery is a convenience type that contains both an Oven 8 | // and a Checker. 9 | type Bakery struct { 10 | Oven *Oven 11 | Checker *Checker 12 | } 13 | 14 | // BakeryParams holds a selection of parameters for the Oven 15 | // and the Checker created by New. 16 | // 17 | // For more fine-grained control of parameters, create the 18 | // Oven or Checker directly. 19 | // 20 | // The zero value is OK to use, but won't allow any authentication 21 | // or third party caveats to be added. 22 | type BakeryParams struct { 23 | // Logger is used to send log messages. If it is nil, 24 | // nothing will be logged. 25 | Logger Logger 26 | 27 | // Checker holds the checker used to check first party caveats. 28 | // If this is nil, New will use checkers.New(nil). 29 | Checker FirstPartyCaveatChecker 30 | 31 | // RootKeyStore holds the root key store to use. If you need to 32 | // use a different root key store for different operations, 33 | // you'll need to pass a RootKeyStoreForOps value to NewOven 34 | // directly. 35 | // 36 | // If this is nil, New will use NewMemRootKeyStore(). 37 | // Note that that is almost certain insufficient for production services 38 | // that are spread across multiple instances or that need 39 | // to persist keys across restarts. 40 | RootKeyStore RootKeyStore 41 | 42 | // Locator is used to find out information on third parties when 43 | // adding third party caveats. If this is nil, no non-local third 44 | // party caveats can be added. 45 | Locator ThirdPartyLocator 46 | 47 | // Key holds the private key of the oven. If this is nil, 48 | // no third party caveats may be added. 49 | Key *KeyPair 50 | 51 | // OpsAuthorizer is used to check whether operations are authorized 52 | // by some other already-authorized operation. If it is nil, 53 | // NewChecker will assume no operation is authorized by any 54 | // operation except itself. 55 | OpsAuthorizer OpsAuthorizer 56 | 57 | // Location holds the location to use when creating new macaroons. 58 | Location string 59 | 60 | // LegacyMacaroonOp holds the operation to associate with old 61 | // macaroons that don't have associated operations. 62 | // If this is empty, legacy macaroons will not be associated 63 | // with any operations. 64 | LegacyMacaroonOp Op 65 | } 66 | 67 | // New returns a new Bakery instance which combines an Oven with a 68 | // Checker for the convenience of callers that wish to use both 69 | // together. 70 | func New(p BakeryParams) *Bakery { 71 | if p.Checker == nil { 72 | p.Checker = checkers.New(nil) 73 | } 74 | ovenParams := OvenParams{ 75 | Key: p.Key, 76 | Namespace: p.Checker.Namespace(), 77 | Location: p.Location, 78 | Locator: p.Locator, 79 | LegacyMacaroonOp: p.LegacyMacaroonOp, 80 | } 81 | if p.RootKeyStore != nil { 82 | ovenParams.RootKeyStoreForOps = func(ops []Op) RootKeyStore { 83 | return p.RootKeyStore 84 | } 85 | } 86 | oven := NewOven(ovenParams) 87 | 88 | checker := NewChecker(CheckerParams{ 89 | Checker: p.Checker, 90 | MacaroonVerifier: oven, 91 | OpsAuthorizer: p.OpsAuthorizer, 92 | }) 93 | return &Bakery{ 94 | Oven: oven, 95 | Checker: checker, 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /bakery/checkers/declared.go: -------------------------------------------------------------------------------- 1 | package checkers 2 | 3 | import ( 4 | "context" 5 | "strings" 6 | 7 | "gopkg.in/errgo.v1" 8 | "gopkg.in/macaroon.v2" 9 | ) 10 | 11 | type macaroonsKey struct{} 12 | 13 | type macaroonsValue struct { 14 | ns *Namespace 15 | ms macaroon.Slice 16 | } 17 | 18 | // ContextWithMacaroons returns the given context associated with a 19 | // macaroon slice and the name space to use to interpret caveats in 20 | // the macaroons. 21 | func ContextWithMacaroons(ctx context.Context, ns *Namespace, ms macaroon.Slice) context.Context { 22 | return context.WithValue(ctx, macaroonsKey{}, macaroonsValue{ 23 | ns: ns, 24 | ms: ms, 25 | }) 26 | } 27 | 28 | // MacaroonsFromContext returns the namespace and macaroons associated 29 | // with the context by ContextWithMacaroons. This can be used to 30 | // implement "structural" first-party caveats that are predicated on 31 | // the macaroons being validated. 32 | func MacaroonsFromContext(ctx context.Context) (*Namespace, macaroon.Slice) { 33 | v, _ := ctx.Value(macaroonsKey{}).(macaroonsValue) 34 | return v.ns, v.ms 35 | } 36 | 37 | // DeclaredCaveat returns a "declared" caveat asserting that the given key is 38 | // set to the given value. If a macaroon has exactly one first party 39 | // caveat asserting the value of a particular key, then InferDeclared 40 | // will be able to infer the value, and then DeclaredChecker will allow 41 | // the declared value if it has the value specified here. 42 | // 43 | // If the key is empty or contains a space, DeclaredCaveat 44 | // will return an error caveat. 45 | func DeclaredCaveat(key string, value string) Caveat { 46 | if strings.Contains(key, " ") || key == "" { 47 | return ErrorCaveatf("invalid caveat 'declared' key %q", key) 48 | } 49 | return firstParty(CondDeclared, key+" "+value) 50 | } 51 | 52 | // NeedDeclaredCaveat returns a third party caveat that 53 | // wraps the provided third party caveat and requires 54 | // that the third party must add "declared" caveats for 55 | // all the named keys. 56 | // TODO(rog) namespaces in third party caveats? 57 | func NeedDeclaredCaveat(cav Caveat, keys ...string) Caveat { 58 | if cav.Location == "" { 59 | return ErrorCaveatf("need-declared caveat is not third-party") 60 | } 61 | return Caveat{ 62 | Location: cav.Location, 63 | Condition: CondNeedDeclared + " " + strings.Join(keys, ",") + " " + cav.Condition, 64 | } 65 | } 66 | 67 | func checkDeclared(ctx context.Context, _, arg string) error { 68 | parts := strings.SplitN(arg, " ", 2) 69 | if len(parts) != 2 { 70 | return errgo.Newf("declared caveat has no value") 71 | } 72 | ns, ms := MacaroonsFromContext(ctx) 73 | attrs := InferDeclared(ns, ms) 74 | val, ok := attrs[parts[0]] 75 | if !ok { 76 | return errgo.Newf("got %s=null, expected %q", parts[0], parts[1]) 77 | } 78 | if val != parts[1] { 79 | return errgo.Newf("got %s=%q, expected %q", parts[0], val, parts[1]) 80 | } 81 | return nil 82 | } 83 | 84 | // InferDeclared retrieves any declared information from 85 | // the given macaroons and returns it as a key-value map. 86 | // 87 | // Information is declared with a first party caveat as created 88 | // by DeclaredCaveat. 89 | // 90 | // If there are two caveats that declare the same key with 91 | // different values, the information is omitted from the map. 92 | // When the caveats are later checked, this will cause the 93 | // check to fail. 94 | func InferDeclared(ns *Namespace, ms macaroon.Slice) map[string]string { 95 | var conditions []string 96 | for _, m := range ms { 97 | for _, cav := range m.Caveats() { 98 | if cav.Location == "" { 99 | conditions = append(conditions, string(cav.Id)) 100 | } 101 | } 102 | } 103 | return InferDeclaredFromConditions(ns, conditions) 104 | } 105 | 106 | // InferDeclaredFromConditions is like InferDeclared except that 107 | // it is passed a set of first party caveat conditions rather than a set of macaroons. 108 | func InferDeclaredFromConditions(ns *Namespace, conds []string) map[string]string { 109 | var conflicts []string 110 | // If we can't resolve that standard namespace, then we'll look for 111 | // just bare "declared" caveats which will work OK for legacy 112 | // macaroons with no namespace. 113 | prefix, _ := ns.Resolve(StdNamespace) 114 | declaredCond := prefix + CondDeclared 115 | 116 | info := make(map[string]string) 117 | for _, cond := range conds { 118 | name, rest, _ := ParseCaveat(cond) 119 | if name != declaredCond { 120 | continue 121 | } 122 | parts := strings.SplitN(rest, " ", 2) 123 | if len(parts) != 2 { 124 | continue 125 | } 126 | key, val := parts[0], parts[1] 127 | if oldVal, ok := info[key]; ok && oldVal != val { 128 | conflicts = append(conflicts, key) 129 | continue 130 | } 131 | info[key] = val 132 | } 133 | for _, key := range conflicts { 134 | delete(info, key) 135 | } 136 | return info 137 | } 138 | -------------------------------------------------------------------------------- /bakery/checkers/namespace.go: -------------------------------------------------------------------------------- 1 | package checkers 2 | 3 | import ( 4 | "sort" 5 | "strings" 6 | "unicode" 7 | "unicode/utf8" 8 | 9 | "gopkg.in/errgo.v1" 10 | ) 11 | 12 | // Namespace holds maps from schema URIs to the 13 | // prefixes that are used to encode them in first party 14 | // caveats. Several different URIs may map to the same 15 | // prefix - this is usual when several different backwardly 16 | // compatible schema versions are registered. 17 | type Namespace struct { 18 | uriToPrefix map[string]string 19 | } 20 | 21 | // Equal reports whether ns2 encodes the same namespace 22 | // as the receiver. 23 | func (ns1 *Namespace) Equal(ns2 *Namespace) bool { 24 | if ns1 == ns2 || ns1 == nil || ns2 == nil { 25 | return ns1 == ns2 26 | } 27 | if len(ns1.uriToPrefix) != len(ns2.uriToPrefix) { 28 | return false 29 | } 30 | for k, v := range ns1.uriToPrefix { 31 | if ns2.uriToPrefix[k] != v { 32 | return false 33 | } 34 | } 35 | return true 36 | } 37 | 38 | // NewNamespace returns a new namespace with the 39 | // given initial contents. It will panic if any of the 40 | // URI keys or their associated prefix are invalid 41 | // (see IsValidSchemaURI and IsValidPrefix). 42 | func NewNamespace(uriToPrefix map[string]string) *Namespace { 43 | ns := &Namespace{ 44 | uriToPrefix: make(map[string]string), 45 | } 46 | for uri, prefix := range uriToPrefix { 47 | ns.Register(uri, prefix) 48 | } 49 | return ns 50 | } 51 | 52 | // String returns the namespace representation as returned by 53 | // ns.MarshalText. 54 | func (ns *Namespace) String() string { 55 | data, _ := ns.MarshalText() 56 | return string(data) 57 | } 58 | 59 | // MarshalText implements encoding.TextMarshaler by 60 | // returning all the elements in the namespace sorted by 61 | // URI, joined to the associated prefix with a colon and 62 | // separated with spaces. 63 | func (ns *Namespace) MarshalText() ([]byte, error) { 64 | if ns == nil || len(ns.uriToPrefix) == 0 { 65 | return nil, nil 66 | } 67 | uris := make([]string, 0, len(ns.uriToPrefix)) 68 | dataLen := 0 69 | for uri, prefix := range ns.uriToPrefix { 70 | uris = append(uris, uri) 71 | dataLen += len(uri) + 1 + len(prefix) + 1 72 | } 73 | sort.Strings(uris) 74 | data := make([]byte, 0, dataLen) 75 | for i, uri := range uris { 76 | if i > 0 { 77 | data = append(data, ' ') 78 | } 79 | data = append(data, uri...) 80 | data = append(data, ':') 81 | data = append(data, ns.uriToPrefix[uri]...) 82 | } 83 | return data, nil 84 | } 85 | 86 | func (ns *Namespace) UnmarshalText(data []byte) error { 87 | uriToPrefix := make(map[string]string) 88 | elems := strings.Fields(string(data)) 89 | for _, elem := range elems { 90 | i := strings.LastIndex(elem, ":") 91 | if i == -1 { 92 | return errgo.Newf("no colon in namespace field %q", elem) 93 | } 94 | uri, prefix := elem[0:i], elem[i+1:] 95 | if !IsValidSchemaURI(uri) { 96 | // Currently this can't happen because the only invalid URIs 97 | // are those which contain a space 98 | return errgo.Newf("invalid URI %q in namespace field %q", uri, elem) 99 | } 100 | if !IsValidPrefix(prefix) { 101 | return errgo.Newf("invalid prefix %q in namespace field %q", prefix, elem) 102 | } 103 | if _, ok := uriToPrefix[uri]; ok { 104 | return errgo.Newf("duplicate URI %q in namespace %q", uri, data) 105 | } 106 | uriToPrefix[uri] = prefix 107 | } 108 | ns.uriToPrefix = uriToPrefix 109 | return nil 110 | } 111 | 112 | // EnsureResolved tries to resolve the given schema URI to a prefix and 113 | // returns the prefix and whether the resolution was successful. If the 114 | // URI hasn't been registered but a compatible version has, the 115 | // given URI is registered with the same prefix. 116 | func (ns *Namespace) EnsureResolved(uri string) (string, bool) { 117 | // TODO(rog) compatibility 118 | return ns.Resolve(uri) 119 | } 120 | 121 | // Resolve resolves the given schema URI to its registered prefix and 122 | // returns the prefix and whether the resolution was successful. 123 | // 124 | // If ns is nil, it is treated as if it were empty. 125 | // 126 | // Resolve does not mutate ns and may be called concurrently 127 | // with other non-mutating Namespace methods. 128 | func (ns *Namespace) Resolve(uri string) (string, bool) { 129 | if ns == nil { 130 | return "", false 131 | } 132 | prefix, ok := ns.uriToPrefix[uri] 133 | return prefix, ok 134 | } 135 | 136 | // ResolveCaveat resolves the given caveat by using 137 | // Resolve to map from its schema namespace to the appropriate prefix using 138 | // Resolve. If there is no registered prefix for the namespace, 139 | // it returns an error caveat. 140 | // 141 | // If ns.Namespace is empty or ns.Location is non-empty, it returns cav unchanged. 142 | // 143 | // If ns is nil, it is treated as if it were empty. 144 | // 145 | // ResolveCaveat does not mutate ns and may be called concurrently 146 | // with other non-mutating Namespace methods. 147 | func (ns *Namespace) ResolveCaveat(cav Caveat) Caveat { 148 | // TODO(rog) If a namespace isn't registered, try to resolve it by 149 | // resolving it to the latest compatible version that is 150 | // registered. 151 | if cav.Namespace == "" || cav.Location != "" { 152 | return cav 153 | } 154 | prefix, ok := ns.Resolve(cav.Namespace) 155 | if !ok { 156 | errCav := ErrorCaveatf("caveat %q in unregistered namespace %q", cav.Condition, cav.Namespace) 157 | if errCav.Namespace != cav.Namespace { 158 | prefix, _ = ns.Resolve(errCav.Namespace) 159 | } 160 | cav = errCav 161 | } 162 | if prefix != "" { 163 | cav.Condition = ConditionWithPrefix(prefix, cav.Condition) 164 | } 165 | cav.Namespace = "" 166 | return cav 167 | } 168 | 169 | // ConditionWithPrefix returns the given string prefixed by the 170 | // given prefix. If the prefix is non-empty, a colon 171 | // is used to separate them. 172 | func ConditionWithPrefix(prefix, condition string) string { 173 | if prefix == "" { 174 | return condition 175 | } 176 | return prefix + ":" + condition 177 | } 178 | 179 | // Register registers the given URI and associates it 180 | // with the given prefix. If the URI has already been registered, 181 | // this is a no-op. 182 | func (ns *Namespace) Register(uri, prefix string) { 183 | if !IsValidSchemaURI(uri) { 184 | panic(errgo.Newf("cannot register invalid URI %q (prefix %q)", uri, prefix)) 185 | } 186 | if !IsValidPrefix(prefix) { 187 | panic(errgo.Newf("cannot register invalid prefix %q for URI %q", prefix, uri)) 188 | } 189 | if _, ok := ns.uriToPrefix[uri]; !ok { 190 | ns.uriToPrefix[uri] = prefix 191 | } 192 | } 193 | 194 | func invalidSchemaRune(r rune) bool { 195 | return unicode.IsSpace(r) 196 | } 197 | 198 | // IsValidSchemaURI reports whether the given argument is suitable for 199 | // use as a namespace schema URI. It must be non-empty, a valid UTF-8 200 | // string and it must not contain white space. 201 | func IsValidSchemaURI(uri string) bool { 202 | // TODO more stringent requirements? 203 | return len(uri) > 0 && 204 | utf8.ValidString(uri) && 205 | strings.IndexFunc(uri, invalidSchemaRune) == -1 206 | } 207 | 208 | func invalidPrefixRune(r rune) bool { 209 | return r == ' ' || r == ':' || unicode.IsSpace(r) 210 | } 211 | 212 | func IsValidPrefix(prefix string) bool { 213 | return utf8.ValidString(prefix) && strings.IndexFunc(prefix, invalidPrefixRune) == -1 214 | } 215 | -------------------------------------------------------------------------------- /bakery/checkers/time.go: -------------------------------------------------------------------------------- 1 | package checkers 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | 8 | "gopkg.in/errgo.v1" 9 | "gopkg.in/macaroon.v2" 10 | ) 11 | 12 | // Clock represents a clock that can be faked for testing purposes. 13 | type Clock interface { 14 | Now() time.Time 15 | } 16 | 17 | type timeKey struct{} 18 | 19 | func ContextWithClock(ctx context.Context, clock Clock) context.Context { 20 | if clock == nil { 21 | return ctx 22 | } 23 | return context.WithValue(ctx, timeKey{}, clock) 24 | } 25 | 26 | func clockFromContext(ctx context.Context) Clock { 27 | c, _ := ctx.Value(timeKey{}).(Clock) 28 | return c 29 | } 30 | 31 | func checkTimeBefore(ctx context.Context, _, arg string) error { 32 | var now time.Time 33 | if clock := clockFromContext(ctx); clock != nil { 34 | now = clock.Now() 35 | } else { 36 | now = time.Now() 37 | } 38 | t, err := time.Parse(time.RFC3339Nano, arg) 39 | if err != nil { 40 | return errgo.Mask(err) 41 | } 42 | if !now.Before(t) { 43 | return fmt.Errorf("macaroon has expired") 44 | } 45 | return nil 46 | } 47 | 48 | // TimeBeforeCaveat returns a caveat that specifies that 49 | // the time that it is checked should be before t. 50 | func TimeBeforeCaveat(t time.Time) Caveat { 51 | return firstParty(CondTimeBefore, t.UTC().Format(time.RFC3339Nano)) 52 | } 53 | 54 | // ExpiryTime returns the minimum time of any time-before caveats found 55 | // in the given slice and whether there were any such caveats found. 56 | // 57 | // The ns parameter is used to determine the standard namespace prefix - if 58 | // the standard namespace is not found, the empty prefix is assumed. 59 | func ExpiryTime(ns *Namespace, cavs []macaroon.Caveat) (time.Time, bool) { 60 | prefix, _ := ns.Resolve(StdNamespace) 61 | timeBeforeCond := ConditionWithPrefix(prefix, CondTimeBefore) 62 | var t time.Time 63 | var expires bool 64 | for _, cav := range cavs { 65 | cav := string(cav.Id) 66 | name, rest, _ := ParseCaveat(cav) 67 | if name != timeBeforeCond { 68 | continue 69 | } 70 | et, err := time.Parse(time.RFC3339Nano, rest) 71 | if err != nil { 72 | continue 73 | } 74 | if !expires || et.Before(t) { 75 | t = et 76 | expires = true 77 | } 78 | } 79 | return t, expires 80 | } 81 | 82 | // MacaroonsExpiryTime returns the minimum time of any time-before 83 | // caveats found in the given macaroons and whether there were 84 | // any such caveats found. 85 | func MacaroonsExpiryTime(ns *Namespace, ms macaroon.Slice) (time.Time, bool) { 86 | var t time.Time 87 | var expires bool 88 | for _, m := range ms { 89 | if et, ex := ExpiryTime(ns, m.Caveats()); ex { 90 | if !expires || et.Before(t) { 91 | t = et 92 | expires = true 93 | } 94 | } 95 | } 96 | return t, expires 97 | } 98 | -------------------------------------------------------------------------------- /bakery/checkers/time_test.go: -------------------------------------------------------------------------------- 1 | package checkers_test 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | qt "github.com/frankban/quicktest" 8 | "gopkg.in/macaroon.v2" 9 | 10 | "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/checkers" 11 | ) 12 | 13 | var t1 = time.Now() 14 | var t2 = t1.Add(1 * time.Hour) 15 | var t3 = t2.Add(1 * time.Hour) 16 | 17 | var expireTimeTests = []struct { 18 | about string 19 | caveats []macaroon.Caveat 20 | expectTime time.Time 21 | expectExpires bool 22 | }{{ 23 | about: "nil caveats", 24 | }, { 25 | about: "empty caveats", 26 | caveats: []macaroon.Caveat{}, 27 | }, { 28 | about: "single time-before caveat", 29 | caveats: []macaroon.Caveat{ 30 | macaroon.Caveat{ 31 | Id: []byte(checkers.TimeBeforeCaveat(t1).Condition), 32 | }, 33 | }, 34 | expectTime: t1, 35 | expectExpires: true, 36 | }, { 37 | about: "multiple time-before caveat", 38 | caveats: []macaroon.Caveat{ 39 | macaroon.Caveat{ 40 | Id: []byte(checkers.TimeBeforeCaveat(t2).Condition), 41 | }, 42 | macaroon.Caveat{ 43 | Id: []byte(checkers.TimeBeforeCaveat(t1).Condition), 44 | }, 45 | }, 46 | expectTime: t1, 47 | expectExpires: true, 48 | }, { 49 | about: "mixed caveats", 50 | caveats: []macaroon.Caveat{ 51 | macaroon.Caveat{ 52 | Id: []byte(checkers.TimeBeforeCaveat(t1).Condition), 53 | }, 54 | macaroon.Caveat{ 55 | Id: []byte("allow bar"), 56 | }, 57 | macaroon.Caveat{ 58 | Id: []byte(checkers.TimeBeforeCaveat(t2).Condition), 59 | }, 60 | macaroon.Caveat{ 61 | Id: []byte("deny foo"), 62 | }, 63 | }, 64 | expectTime: t1, 65 | expectExpires: true, 66 | }, { 67 | about: "invalid time-before caveat", 68 | caveats: []macaroon.Caveat{ 69 | macaroon.Caveat{ 70 | Id: []byte(checkers.CondTimeBefore + " tomorrow"), 71 | }, 72 | }, 73 | }} 74 | 75 | func TestExpireTime(t *testing.T) { 76 | c := qt.New(t) 77 | for i, test := range expireTimeTests { 78 | c.Logf("%d. %s", i, test.about) 79 | t, expires := checkers.ExpiryTime(nil, test.caveats) 80 | c.Assert(t.Equal(test.expectTime), qt.Equals, true, qt.Commentf("obtained: %s, expected: %s", t, test.expectTime)) 81 | c.Assert(expires, qt.Equals, test.expectExpires) 82 | } 83 | } 84 | 85 | var macaroonsExpireTimeTests = []struct { 86 | about string 87 | macaroons macaroon.Slice 88 | expectTime time.Time 89 | expectExpires bool 90 | }{{ 91 | about: "nil macaroons", 92 | }, { 93 | about: "empty macaroons", 94 | macaroons: macaroon.Slice{}, 95 | }, { 96 | about: "single macaroon without caveats", 97 | macaroons: macaroon.Slice{ 98 | mustNewMacaroon(), 99 | }, 100 | }, { 101 | about: "multiple macaroon without caveats", 102 | macaroons: macaroon.Slice{ 103 | mustNewMacaroon(), 104 | mustNewMacaroon(), 105 | }, 106 | }, { 107 | about: "single macaroon with time-before caveat", 108 | macaroons: macaroon.Slice{ 109 | mustNewMacaroon( 110 | checkers.TimeBeforeCaveat(t1).Condition, 111 | ), 112 | }, 113 | expectTime: t1, 114 | expectExpires: true, 115 | }, { 116 | about: "single macaroon with multiple time-before caveats", 117 | macaroons: macaroon.Slice{ 118 | mustNewMacaroon( 119 | checkers.TimeBeforeCaveat(t2).Condition, 120 | checkers.TimeBeforeCaveat(t1).Condition, 121 | ), 122 | }, 123 | expectTime: t1, 124 | expectExpires: true, 125 | }, { 126 | about: "multiple macaroons with multiple time-before caveats", 127 | macaroons: macaroon.Slice{ 128 | mustNewMacaroon( 129 | checkers.TimeBeforeCaveat(t3).Condition, 130 | checkers.TimeBeforeCaveat(t2).Condition, 131 | ), 132 | mustNewMacaroon( 133 | checkers.TimeBeforeCaveat(t3).Condition, 134 | checkers.TimeBeforeCaveat(t1).Condition, 135 | ), 136 | }, 137 | expectTime: t1, 138 | expectExpires: true, 139 | }} 140 | 141 | func TestMacaroonsExpireTime(t *testing.T) { 142 | c := qt.New(t) 143 | for i, test := range macaroonsExpireTimeTests { 144 | c.Logf("%d. %s", i, test.about) 145 | t, expires := checkers.MacaroonsExpiryTime(nil, test.macaroons) 146 | c.Assert(t.Equal(test.expectTime), qt.Equals, true, qt.Commentf("obtained: %s, expected: %s", t, test.expectTime)) 147 | c.Assert(expires, qt.Equals, test.expectExpires) 148 | } 149 | } 150 | 151 | func mustNewMacaroon(cavs ...string) *macaroon.Macaroon { 152 | m, err := macaroon.New(nil, nil, "", macaroon.LatestVersion) 153 | if err != nil { 154 | panic(err) 155 | } 156 | for _, cav := range cavs { 157 | if err := m.AddFirstPartyCaveat([]byte(cav)); err != nil { 158 | panic(err) 159 | } 160 | } 161 | return m 162 | } 163 | -------------------------------------------------------------------------------- /bakery/codec_test.go: -------------------------------------------------------------------------------- 1 | package bakery 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | qt "github.com/frankban/quicktest" 8 | "golang.org/x/crypto/nacl/box" 9 | 10 | "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/checkers" 11 | ) 12 | 13 | var ( 14 | testFirstPartyKey = MustGenerateKey() 15 | testThirdPartyKey = MustGenerateKey() 16 | ) 17 | 18 | func TestV1RoundTrip(t *testing.T) { 19 | c := qt.New(t) 20 | cid, err := encodeCaveatV1( 21 | "is-authenticated-user", []byte("a random string"), &testThirdPartyKey.Public, testFirstPartyKey) 22 | 23 | c.Assert(err, qt.IsNil) 24 | 25 | res, err := decodeCaveat(testThirdPartyKey, cid) 26 | c.Assert(err, qt.IsNil) 27 | c.Assert(res, qt.DeepEquals, &ThirdPartyCaveatInfo{ 28 | FirstPartyPublicKey: testFirstPartyKey.Public, 29 | RootKey: []byte("a random string"), 30 | Condition: []byte("is-authenticated-user"), 31 | Caveat: cid, 32 | ThirdPartyKeyPair: *testThirdPartyKey, 33 | Version: Version1, 34 | Namespace: legacyNamespace(), 35 | }) 36 | } 37 | 38 | func TestV2RoundTrip(t *testing.T) { 39 | c := qt.New(t) 40 | cid, err := encodeCaveatV2("is-authenticated-user", []byte("a random string"), &testThirdPartyKey.Public, testFirstPartyKey) 41 | 42 | c.Assert(err, qt.IsNil) 43 | 44 | res, err := decodeCaveat(testThirdPartyKey, cid) 45 | c.Assert(err, qt.IsNil) 46 | c.Assert(res, qt.DeepEquals, &ThirdPartyCaveatInfo{ 47 | FirstPartyPublicKey: testFirstPartyKey.Public, 48 | RootKey: []byte("a random string"), 49 | Condition: []byte("is-authenticated-user"), 50 | Caveat: cid, 51 | ThirdPartyKeyPair: *testThirdPartyKey, 52 | Version: Version2, 53 | Namespace: legacyNamespace(), 54 | }) 55 | } 56 | 57 | func TestV3RoundTrip(t *testing.T) { 58 | c := qt.New(t) 59 | ns := checkers.NewNamespace(nil) 60 | ns.Register("testns", "x") 61 | cid, err := encodeCaveatV3("is-authenticated-user", []byte("a random string"), &testThirdPartyKey.Public, testFirstPartyKey, ns) 62 | 63 | c.Assert(err, qt.IsNil) 64 | c.Logf("cid %x", cid) 65 | 66 | res, err := decodeCaveat(testThirdPartyKey, cid) 67 | c.Assert(err, qt.IsNil) 68 | c.Assert(res, qt.DeepEquals, &ThirdPartyCaveatInfo{ 69 | FirstPartyPublicKey: testFirstPartyKey.Public, 70 | RootKey: []byte("a random string"), 71 | Condition: []byte("is-authenticated-user"), 72 | Caveat: cid, 73 | ThirdPartyKeyPair: *testThirdPartyKey, 74 | Version: Version3, 75 | Namespace: ns, 76 | }) 77 | } 78 | 79 | func TestEmptyCaveatId(t *testing.T) { 80 | c := qt.New(t) 81 | _, err := decodeCaveat(testThirdPartyKey, []byte{}) 82 | c.Assert(err, qt.ErrorMatches, "empty third party caveat") 83 | } 84 | 85 | func TestCaveatIdBadVersion(t *testing.T) { 86 | c := qt.New(t) 87 | _, err := decodeCaveat(testThirdPartyKey, []byte{1}) 88 | c.Assert(err, qt.ErrorMatches, "caveat has unsupported version 1") 89 | } 90 | 91 | func TestV2TooShort(t *testing.T) { 92 | c := qt.New(t) 93 | _, err := decodeCaveat(testThirdPartyKey, []byte{2}) 94 | c.Assert(err, qt.ErrorMatches, "caveat id too short") 95 | } 96 | 97 | func TestV2BadKey(t *testing.T) { 98 | c := qt.New(t) 99 | cid, err := encodeCaveatV2("is-authenticated-user", []byte("a random string"), &testThirdPartyKey.Public, testFirstPartyKey) 100 | 101 | c.Assert(err, qt.IsNil) 102 | cid[1] ^= 1 103 | 104 | _, err = decodeCaveat(testThirdPartyKey, cid) 105 | c.Assert(err, qt.ErrorMatches, "public key mismatch") 106 | } 107 | 108 | func TestV2DecryptionError(t *testing.T) { 109 | c := qt.New(t) 110 | cid, err := encodeCaveatV2("is-authenticated-user", []byte("a random string"), &testThirdPartyKey.Public, testFirstPartyKey) 111 | 112 | c.Assert(err, qt.IsNil) 113 | cid[5] ^= 1 114 | 115 | _, err = decodeCaveat(testThirdPartyKey, cid) 116 | c.Assert(err, qt.ErrorMatches, "cannot decrypt caveat id") 117 | } 118 | 119 | func TestV2EmptySecretPart(t *testing.T) { 120 | c := qt.New(t) 121 | cid, err := encodeCaveatV2("is-authenticated-user", []byte("a random string"), &testThirdPartyKey.Public, testFirstPartyKey) 122 | 123 | c.Assert(err, qt.IsNil) 124 | cid = replaceV2SecretPart(cid, []byte{}) 125 | 126 | _, err = decodeCaveat(testThirdPartyKey, cid) 127 | c.Assert(err, qt.ErrorMatches, "invalid secret part: secret part too short") 128 | } 129 | 130 | func TestV2BadSecretPartVersion(t *testing.T) { 131 | c := qt.New(t) 132 | cid, err := encodeCaveatV2("is-authenticated-user", []byte("a random string"), &testThirdPartyKey.Public, testFirstPartyKey) 133 | c.Assert(err, qt.IsNil) 134 | cid = replaceV2SecretPart(cid, []byte{1}) 135 | 136 | _, err = decodeCaveat(testThirdPartyKey, cid) 137 | c.Assert(err, qt.ErrorMatches, "invalid secret part: unexpected secret part version, got 1 want 2") 138 | } 139 | 140 | func TestV2EmptyRootKey(t *testing.T) { 141 | c := qt.New(t) 142 | cid, err := encodeCaveatV2("is-authenticated-user", []byte{}, &testThirdPartyKey.Public, testFirstPartyKey) 143 | c.Assert(err, qt.IsNil) 144 | 145 | res, err := decodeCaveat(testThirdPartyKey, cid) 146 | c.Assert(err, qt.IsNil) 147 | c.Assert(res, qt.DeepEquals, &ThirdPartyCaveatInfo{ 148 | FirstPartyPublicKey: testFirstPartyKey.Public, 149 | RootKey: []byte{}, 150 | Condition: []byte("is-authenticated-user"), 151 | Caveat: cid, 152 | ThirdPartyKeyPair: *testThirdPartyKey, 153 | Version: Version2, 154 | Namespace: legacyNamespace(), 155 | }) 156 | } 157 | 158 | func TestV2LongRootKey(t *testing.T) { 159 | c := qt.New(t) 160 | cid, err := encodeCaveatV2("is-authenticated-user", bytes.Repeat([]byte{0}, 65536), &testThirdPartyKey.Public, testFirstPartyKey) 161 | c.Assert(err, qt.IsNil) 162 | 163 | res, err := decodeCaveat(testThirdPartyKey, cid) 164 | c.Assert(err, qt.IsNil) 165 | c.Assert(res, qt.DeepEquals, &ThirdPartyCaveatInfo{ 166 | FirstPartyPublicKey: testFirstPartyKey.Public, 167 | RootKey: bytes.Repeat([]byte{0}, 65536), 168 | Condition: []byte("is-authenticated-user"), 169 | Caveat: cid, 170 | ThirdPartyKeyPair: *testThirdPartyKey, 171 | Version: Version2, 172 | Namespace: legacyNamespace(), 173 | }) 174 | } 175 | 176 | func replaceV2SecretPart(cid, replacement []byte) []byte { 177 | cid = cid[:1+publicKeyPrefixLen+KeyLen+NonceLen] 178 | var nonce [NonceLen]byte 179 | copy(nonce[:], cid[1+publicKeyPrefixLen+KeyLen:]) 180 | return box.Seal(cid, replacement, &nonce, testFirstPartyKey.Public.boxKey(), testThirdPartyKey.Private.boxKey()) 181 | } 182 | -------------------------------------------------------------------------------- /bakery/common_test.go: -------------------------------------------------------------------------------- 1 | package bakery_test 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "time" 8 | 9 | qt "github.com/frankban/quicktest" 10 | "gopkg.in/macaroon.v2" 11 | 12 | "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" 13 | "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/checkers" 14 | ) 15 | 16 | // testContext holds the testing background context - its associated time when checking 17 | // time-before caveats will always be the value of epoch. 18 | var testContext = checkers.ContextWithClock(context.Background(), stoppedClock{epoch}) 19 | 20 | var basicOp = bakery.Op{"basic", "basic"} 21 | 22 | var ( 23 | epoch = time.Date(1900, 11, 17, 19, 00, 13, 0, time.UTC) 24 | ) 25 | 26 | var testChecker = func() *checkers.Checker { 27 | c := checkers.New(nil) 28 | c.Namespace().Register("testns", "") 29 | c.Register("str", "testns", strCheck) 30 | c.Register("true", "testns", trueCheck) 31 | return c 32 | }() 33 | 34 | // newBakery returns a new Bakery instance using a new 35 | // key pair, and registers the key with the given locator if provided. 36 | // 37 | // It uses testChecker to check first party caveats. 38 | func newBakery(location string, locator *bakery.ThirdPartyStore) *bakery.Bakery { 39 | key := mustGenerateKey() 40 | p := bakery.BakeryParams{ 41 | Key: key, 42 | Checker: testChecker, 43 | Location: location, 44 | } 45 | if locator != nil { 46 | p.Locator = locator 47 | locator.AddInfo(location, bakery.ThirdPartyInfo{ 48 | PublicKey: key.Public, 49 | Version: bakery.LatestVersion, 50 | }) 51 | } 52 | return bakery.New(p) 53 | } 54 | 55 | func noDischarge(c *qt.C) func(context.Context, macaroon.Caveat, []byte) (*bakery.Macaroon, error) { 56 | return func(context.Context, macaroon.Caveat, []byte) (*bakery.Macaroon, error) { 57 | c.Errorf("getDischarge called unexpectedly") 58 | return nil, fmt.Errorf("nothing") 59 | } 60 | } 61 | 62 | type strKey struct{} 63 | 64 | func strContext(s string) context.Context { 65 | return context.WithValue(testContext, strKey{}, s) 66 | } 67 | 68 | func strCaveat(s string) checkers.Caveat { 69 | return checkers.Caveat{ 70 | Condition: "str " + s, 71 | Namespace: "testns", 72 | } 73 | } 74 | 75 | func trueCaveat(s string) checkers.Caveat { 76 | return checkers.Caveat{ 77 | Condition: "true " + s, 78 | Namespace: "testns", 79 | } 80 | } 81 | 82 | // trueCheck always succeeds. 83 | func trueCheck(ctx context.Context, cond, args string) error { 84 | return nil 85 | } 86 | 87 | // strCheck checks that the string value in the context 88 | // matches the argument to the condition. 89 | func strCheck(ctx context.Context, cond, args string) error { 90 | expect, _ := ctx.Value(strKey{}).(string) 91 | if args != expect { 92 | return fmt.Errorf("%s doesn't match %s", cond, expect) 93 | } 94 | return nil 95 | } 96 | 97 | type thirdPartyStrcmpChecker string 98 | 99 | func (c thirdPartyStrcmpChecker) CheckThirdPartyCaveat(_ context.Context, cavInfo *bakery.ThirdPartyCaveatInfo) ([]checkers.Caveat, error) { 100 | if string(cavInfo.Condition) != string(c) { 101 | return nil, fmt.Errorf("%s doesn't match %s", cavInfo.Condition, c) 102 | } 103 | return nil, nil 104 | } 105 | 106 | type thirdPartyCheckerWithCaveats []checkers.Caveat 107 | 108 | func (c thirdPartyCheckerWithCaveats) CheckThirdPartyCaveat(_ context.Context, cavInfo *bakery.ThirdPartyCaveatInfo) ([]checkers.Caveat, error) { 109 | return c, nil 110 | } 111 | 112 | func macStr(m *macaroon.Macaroon) string { 113 | data, err := json.MarshalIndent(m, "\t", "\t") 114 | if err != nil { 115 | panic(err) 116 | } 117 | return string(data) 118 | } 119 | 120 | type stoppedClock struct { 121 | t time.Time 122 | } 123 | 124 | func (t stoppedClock) Now() time.Time { 125 | return t.t 126 | } 127 | 128 | func mustGenerateKey() *bakery.KeyPair { 129 | return bakery.MustGenerateKey() 130 | } 131 | -------------------------------------------------------------------------------- /bakery/dischargeall.go: -------------------------------------------------------------------------------- 1 | package bakery 2 | 3 | import ( 4 | "context" 5 | 6 | "gopkg.in/errgo.v1" 7 | "gopkg.in/macaroon.v2" 8 | 9 | "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/checkers" 10 | ) 11 | 12 | // DischargeAll gathers discharge macaroons for all the third party 13 | // caveats in m (and any subsequent caveats required by those) using 14 | // getDischarge to acquire each discharge macaroon. It returns a slice 15 | // with m as the first element, followed by all the discharge macaroons. 16 | // All the discharge macaroons will be bound to the primary macaroon. 17 | // 18 | // The getDischarge function is passed the caveat to be discharged; 19 | // encryptedCaveat will be passed the external caveat payload found 20 | // in m, if any. 21 | func DischargeAll( 22 | ctx context.Context, 23 | m *Macaroon, 24 | getDischarge func(ctx context.Context, cav macaroon.Caveat, encryptedCaveat []byte) (*Macaroon, error), 25 | ) (macaroon.Slice, error) { 26 | return DischargeAllWithKey(ctx, m, getDischarge, nil) 27 | } 28 | 29 | // DischargeAllWithKey is like DischargeAll except that the localKey 30 | // parameter may optionally hold the key of the client, in which case it 31 | // will be used to discharge any third party caveats with the special 32 | // location "local". In this case, the caveat itself must be "true". This 33 | // can be used be a server to ask a client to prove ownership of the 34 | // private key. 35 | // 36 | // When localKey is nil, DischargeAllWithKey is exactly the same as 37 | // DischargeAll. 38 | func DischargeAllWithKey( 39 | ctx context.Context, 40 | m *Macaroon, 41 | getDischarge func(ctx context.Context, cav macaroon.Caveat, encodedCaveat []byte) (*Macaroon, error), 42 | localKey *KeyPair, 43 | ) (macaroon.Slice, error) { 44 | discharges, err := Slice{m}.DischargeAll(ctx, getDischarge, localKey) 45 | if err != nil { 46 | return nil, errgo.Mask(err, errgo.Any) 47 | } 48 | return discharges.Bind(), nil 49 | } 50 | 51 | var localDischargeChecker = ThirdPartyCaveatCheckerFunc(func(_ context.Context, info *ThirdPartyCaveatInfo) ([]checkers.Caveat, error) { 52 | if string(info.Condition) != "true" { 53 | return nil, checkers.ErrCaveatNotRecognized 54 | } 55 | return nil, nil 56 | }) 57 | -------------------------------------------------------------------------------- /bakery/dischargeall_test.go: -------------------------------------------------------------------------------- 1 | package bakery_test 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "testing" 7 | 8 | qt "github.com/frankban/quicktest" 9 | "gopkg.in/errgo.v1" 10 | "gopkg.in/macaroon.v2" 11 | 12 | "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" 13 | "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/checkers" 14 | ) 15 | 16 | func alwaysOK(string) error { 17 | return nil 18 | } 19 | 20 | func TestDischargeAllNoDischarges(t *testing.T) { 21 | c := qt.New(t) 22 | rootKey := []byte("root key") 23 | m, err := bakery.NewMacaroon(rootKey, []byte("id0"), "loc0", bakery.LatestVersion, testChecker.Namespace()) 24 | c.Assert(err, qt.IsNil) 25 | ms, err := bakery.DischargeAll(testContext, m, noDischarge(c)) 26 | c.Assert(err, qt.IsNil) 27 | c.Assert(ms, qt.HasLen, 1) 28 | c.Assert(ms[0].Signature(), qt.DeepEquals, m.M().Signature()) 29 | 30 | err = m.M().Verify(rootKey, alwaysOK, nil) 31 | c.Assert(err, qt.IsNil) 32 | } 33 | 34 | func TestDischargeAllManyDischarges(t *testing.T) { 35 | c := qt.New(t) 36 | rootKey := []byte("root key") 37 | m0, err := bakery.NewMacaroon(rootKey, []byte("id0"), "loc0", bakery.LatestVersion, nil) 38 | c.Assert(err, qt.IsNil) 39 | totalRequired := 40 40 | id := 1 41 | addCaveats := func(m *bakery.Macaroon) { 42 | for i := 0; i < 2; i++ { 43 | if totalRequired == 0 { 44 | break 45 | } 46 | cid := fmt.Sprint("id", id) 47 | err := m.M().AddThirdPartyCaveat([]byte("root key "+cid), []byte(cid), "somewhere") 48 | c.Assert(err, qt.IsNil) 49 | id++ 50 | totalRequired-- 51 | } 52 | } 53 | addCaveats(m0) 54 | getDischarge := func(_ context.Context, cav macaroon.Caveat, payload []byte) (*bakery.Macaroon, error) { 55 | c.Check(payload, qt.IsNil) 56 | m, err := bakery.NewMacaroon([]byte("root key "+string(cav.Id)), cav.Id, "", bakery.LatestVersion, nil) 57 | c.Assert(err, qt.IsNil) 58 | addCaveats(m) 59 | return m, nil 60 | } 61 | ms, err := bakery.DischargeAll(testContext, m0, getDischarge) 62 | c.Assert(err, qt.IsNil) 63 | c.Assert(ms, qt.HasLen, 41) 64 | 65 | err = ms[0].Verify(rootKey, alwaysOK, ms[1:]) 66 | c.Assert(err, qt.IsNil) 67 | } 68 | 69 | func TestDischargeAllManyDischargesWithRealThirdPartyCaveats(t *testing.T) { 70 | c := qt.New(t) 71 | // This is the same flow as TestDischargeAllManyDischarges except that we're 72 | // using actual third party caveats as added by Macaroon.AddCaveat and 73 | // we use a larger number of caveats so that caveat ids will need to get larger. 74 | locator := bakery.NewThirdPartyStore() 75 | bakeries := make(map[string]*bakery.Bakery) 76 | bakeryId := 0 77 | addBakery := func() string { 78 | bakeryId++ 79 | loc := fmt.Sprint("loc", bakeryId) 80 | bakeries[loc] = newBakery(loc, locator) 81 | return loc 82 | } 83 | ts := newBakery("ts-loc", locator) 84 | const totalDischargesRequired = 40 85 | stillRequired := totalDischargesRequired 86 | checker := func(_ context.Context, ci *bakery.ThirdPartyCaveatInfo) (caveats []checkers.Caveat, _ error) { 87 | if string(ci.Condition) != "something" { 88 | return nil, errgo.Newf("unexpected condition") 89 | } 90 | for i := 0; i < 3; i++ { 91 | if stillRequired <= 0 { 92 | break 93 | } 94 | caveats = append(caveats, checkers.Caveat{ 95 | Location: addBakery(), 96 | Condition: "something", 97 | }) 98 | stillRequired-- 99 | } 100 | return caveats, nil 101 | } 102 | 103 | rootKey := []byte("root key") 104 | m0, err := bakery.NewMacaroon(rootKey, []byte("id0"), "ts-loc", bakery.LatestVersion, nil) 105 | c.Assert(err, qt.IsNil) 106 | err = m0.AddCaveat(testContext, checkers.Caveat{ 107 | Location: addBakery(), 108 | Condition: "something", 109 | }, ts.Oven.Key(), locator) 110 | c.Assert(err, qt.IsNil) 111 | // We've added a caveat (the first) so one less caveat is required. 112 | stillRequired-- 113 | getDischarge := func(ctx context.Context, cav macaroon.Caveat, payload []byte) (*bakery.Macaroon, error) { 114 | return bakery.Discharge(ctx, bakery.DischargeParams{ 115 | Id: cav.Id, 116 | Caveat: payload, 117 | Key: bakeries[cav.Location].Oven.Key(), 118 | Checker: bakery.ThirdPartyCaveatCheckerFunc(checker), 119 | Locator: locator, 120 | }) 121 | } 122 | ms, err := bakery.DischargeAll(testContext, m0, getDischarge) 123 | c.Assert(err, qt.IsNil) 124 | c.Assert(ms, qt.HasLen, totalDischargesRequired+1) 125 | 126 | err = ms[0].Verify(rootKey, alwaysOK, ms[1:]) 127 | c.Assert(err, qt.IsNil) 128 | } 129 | 130 | func TestDischargeAllLocalDischarge(t *testing.T) { 131 | c := qt.New(t) 132 | oc := newBakery("ts", nil) 133 | 134 | clientKey, err := bakery.GenerateKey() 135 | c.Assert(err, qt.IsNil) 136 | 137 | m, err := oc.Oven.NewMacaroon(testContext, bakery.LatestVersion, []checkers.Caveat{ 138 | bakery.LocalThirdPartyCaveat(&clientKey.Public, bakery.LatestVersion), 139 | }, basicOp) 140 | c.Assert(err, qt.IsNil) 141 | 142 | ms, err := bakery.DischargeAllWithKey(testContext, m, noDischarge(c), clientKey) 143 | c.Assert(err, qt.IsNil) 144 | 145 | _, err = oc.Checker.Auth(ms).Allow(testContext, basicOp) 146 | c.Assert(err, qt.IsNil) 147 | } 148 | 149 | func TestDischargeAllLocalDischargeVersion1(t *testing.T) { 150 | c := qt.New(t) 151 | oc := newBakery("ts", nil) 152 | 153 | clientKey, err := bakery.GenerateKey() 154 | c.Assert(err, qt.IsNil) 155 | 156 | m, err := oc.Oven.NewMacaroon(testContext, bakery.Version1, []checkers.Caveat{ 157 | bakery.LocalThirdPartyCaveat(&clientKey.Public, bakery.Version1), 158 | }, basicOp) 159 | c.Assert(err, qt.IsNil) 160 | 161 | ms, err := bakery.DischargeAllWithKey(testContext, m, noDischarge(c), clientKey) 162 | c.Assert(err, qt.IsNil) 163 | 164 | _, err = oc.Checker.Auth(ms).Allow(testContext, basicOp) 165 | c.Assert(err, qt.IsNil) 166 | } 167 | -------------------------------------------------------------------------------- /bakery/doc.go: -------------------------------------------------------------------------------- 1 | // The bakery package layers on top of the macaroon package, providing 2 | // a transport and store-agnostic way of using macaroons to assert 3 | // client capabilities. 4 | // 5 | // Summary 6 | // 7 | // The Bakery type is probably where you want to start. 8 | // It encapsulates a Checker type, which performs checking 9 | // of operations, and an Oven type, which encapsulates 10 | // the actual details of the macaroon encoding conventions. 11 | // 12 | // Most other types and functions are designed either to plug 13 | // into one of the above types (the various Authorizer 14 | // implementations, for example), or to expose some independent 15 | // functionality that's potentially useful (Discharge, for example). 16 | // 17 | // The rest of this introduction introduces some of the concepts 18 | // used by the bakery package. 19 | // 20 | // Identity and entities 21 | // 22 | // An Identity represents some authenticated user (or agent), usually 23 | // the client in a network protocol. An identity can be authenticated by 24 | // an external identity server (with a third party macaroon caveat) or 25 | // by locally provided information such as a username and password. 26 | // 27 | // The Checker type is not responsible for determining identity - that 28 | // functionality is represented by the IdentityClient interface. 29 | // 30 | // The Checker uses identities to decide whether something should be 31 | // allowed or not - the Authorizer interface is used to ask whether a 32 | // given identity should be allowed to perform some set of operations. 33 | // 34 | // Operations 35 | // 36 | // An operation defines some requested action on an entity. For example, 37 | // if file system server defines an entity for every file in the server, 38 | // an operation to read a file might look like: 39 | // 40 | // Op{ 41 | // Entity: "/foo", 42 | // Action: "write", 43 | // } 44 | // 45 | // The exact set of entities and actions is up to the caller, but should 46 | // be kept stable over time because authorization tokens will contain 47 | // these names. 48 | // 49 | // To authorize some request on behalf of a remote user, first find out 50 | // what operations that request needs to perform. For example, if the 51 | // user tries to delete a file, the entity might be the path to the 52 | // file's directory and the action might be "write". It may often be 53 | // possible to determine the operations required by a request without 54 | // reference to anything external, when the request itself contains all 55 | // the necessary information. 56 | // 57 | // The LoginOp operation is special - any macaroon associated with this 58 | // operation is treated as a bearer of identity information. If two 59 | // valid LoginOp macaroons are presented, only the first one will be 60 | // used for identity. 61 | // 62 | // Authorization 63 | // 64 | // The Authorizer interface is responsible for determining whether a 65 | // given authenticated identity is authorized to perform a set of 66 | // operations. This is used when the macaroons provided to Auth are not 67 | // sufficient to authorize the operations themselves. 68 | // 69 | // Capabilities 70 | // 71 | // A "capability" is represented by a macaroon that's associated with 72 | // one or more operations, and grants the capability to perform all 73 | // those operations. The AllowCapability method reports whether a 74 | // capability is allowed. It takes into account any authenticated 75 | // identity and any other capabilities provided. 76 | // 77 | // Third party caveats 78 | // 79 | // Sometimes authorization will only be granted if a third party caveat 80 | // is discharged. This will happen when an IdentityClient or Authorizer 81 | // returns a third party caveat. 82 | // 83 | // When this happens, a DischargeRequiredError will be returned 84 | // containing the caveats and the operations required. The caller is 85 | // responsible for creating a macaroon with those caveats associated 86 | // with those operations and for passing that macaroon to the client to 87 | // discharge. 88 | package bakery 89 | -------------------------------------------------------------------------------- /bakery/error.go: -------------------------------------------------------------------------------- 1 | package bakery 2 | 3 | import ( 4 | "fmt" 5 | 6 | "gopkg.in/errgo.v1" 7 | 8 | "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/checkers" 9 | ) 10 | 11 | var ( 12 | // ErrNotFound is returned by Store.Get implementations 13 | // to signal that an id has not been found. 14 | ErrNotFound = errgo.New("not found") 15 | 16 | // ErrPermissionDenied is returned from AuthChecker when 17 | // permission has been denied. 18 | ErrPermissionDenied = errgo.New("permission denied") 19 | ) 20 | 21 | // DischargeRequiredError is returned when authorization has failed and a 22 | // discharged macaroon might fix it. 23 | // 24 | // A caller should grant the user the ability to authorize by minting a 25 | // macaroon associated with Ops (see MacaroonStore.MacaroonIdInfo for 26 | // how the associated operations are retrieved) and adding Caveats. If 27 | // the user succeeds in discharging the caveats, the authorization will 28 | // be granted. 29 | type DischargeRequiredError struct { 30 | // Message holds some reason why the authorization was denied. 31 | // TODO this is insufficient (and maybe unnecessary) because we 32 | // can have multiple errors. 33 | Message string 34 | 35 | // Ops holds all the operations that were not authorized. 36 | // If Ops contains a single LoginOp member, the macaroon 37 | // should be treated as an login token. Login tokens (also 38 | // known as authentication macaroons) usually have a longer 39 | // life span than other macaroons. 40 | Ops []Op 41 | 42 | // Caveats holds the caveats that must be added 43 | // to macaroons that authorize the above operations. 44 | Caveats []checkers.Caveat 45 | 46 | // ForAuthentication holds whether the macaroon holding 47 | // the discharges will be used for authentication, and hence 48 | // should have wider scope and longer lifetime. 49 | // The bakery package never sets this field, but bakery/identchecker 50 | // uses it. 51 | ForAuthentication bool 52 | } 53 | 54 | func (e *DischargeRequiredError) Error() string { 55 | return "macaroon discharge required: " + e.Message 56 | } 57 | 58 | func IsDischargeRequiredError(err error) bool { 59 | _, ok := err.(*DischargeRequiredError) 60 | return ok 61 | } 62 | 63 | // VerificationError is used to signify that an error is because 64 | // of a verification failure rather than because verification 65 | // could not be done. 66 | type VerificationError struct { 67 | Reason error 68 | } 69 | 70 | func (e *VerificationError) Error() string { 71 | return fmt.Sprintf("verification failed: %v", e.Reason) 72 | } 73 | 74 | func isVerificationError(err error) bool { 75 | _, ok := errgo.Cause(err).(*VerificationError) 76 | return ok 77 | } 78 | -------------------------------------------------------------------------------- /bakery/example/authservice.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | 7 | "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" 8 | "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/checkers" 9 | "github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery" 10 | ) 11 | 12 | // authService implements an authorization service, 13 | // that can discharge third-party caveats added 14 | // to other macaroons. 15 | func authService(endpoint string, key *bakery.KeyPair) (http.Handler, error) { 16 | d := httpbakery.NewDischarger(httpbakery.DischargerParams{ 17 | Checker: httpbakery.ThirdPartyCaveatCheckerFunc(thirdPartyChecker), 18 | Key: bakery.MustGenerateKey(), 19 | }) 20 | 21 | mux := http.NewServeMux() 22 | d.AddMuxHandlers(mux, "/") 23 | return mux, nil 24 | } 25 | 26 | // thirdPartyChecker is used to check third party caveats added by other 27 | // services. The HTTP request is that of the client - it is attempting 28 | // to gather a discharge macaroon. 29 | // 30 | // Note how this function can return additional first- and third-party 31 | // caveats which will be added to the original macaroon's caveats. 32 | func thirdPartyChecker(ctx context.Context, req *http.Request, info *bakery.ThirdPartyCaveatInfo, token *httpbakery.DischargeToken) ([]checkers.Caveat, error) { 33 | if string(info.Condition) != "access-allowed" { 34 | return nil, checkers.ErrCaveatNotRecognized 35 | } 36 | // TODO check that the HTTP request has cookies that prove 37 | // something about the client. 38 | return []checkers.Caveat{ 39 | httpbakery.SameClientIPAddrCaveat(req), 40 | }, nil 41 | } 42 | -------------------------------------------------------------------------------- /bakery/example/client.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "net/http" 7 | 8 | "gopkg.in/errgo.v1" 9 | 10 | "github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery" 11 | ) 12 | 13 | // client represents a client of the target service. 14 | // In this simple example, it just tries a GET 15 | // request, which will fail unless the client 16 | // has the required authorization. 17 | func clientRequest(client *httpbakery.Client, serverEndpoint string) (string, error) { 18 | // The Do function implements the mechanics 19 | // of actually gathering discharge macaroons 20 | // when required, and retrying the request 21 | // when necessary. 22 | req, err := http.NewRequest("GET", serverEndpoint, nil) 23 | if err != nil { 24 | return "", errgo.Notef(err, "cannot make new HTTP request") 25 | } 26 | resp, err := client.Do(req) 27 | if err != nil { 28 | return "", errgo.NoteMask(err, "GET failed", errgo.Any) 29 | } 30 | defer resp.Body.Close() 31 | // TODO(rog) unmarshal error 32 | data, err := ioutil.ReadAll(resp.Body) 33 | if err != nil { 34 | return "", fmt.Errorf("cannot read response: %v", err) 35 | } 36 | return string(data), nil 37 | } 38 | -------------------------------------------------------------------------------- /bakery/example/example_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net/http" 5 | "testing" 6 | 7 | qt "github.com/frankban/quicktest" 8 | 9 | "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" 10 | ) 11 | 12 | func TestExample(t *testing.T) { 13 | c := qt.New(t) 14 | f := newFixture(c) 15 | client := newClient() 16 | serverEndpoint, err := serve(func(endpoint string) (http.Handler, error) { 17 | return targetService(endpoint, f.authEndpoint, f.authPublicKey) 18 | }) 19 | c.Assert(err, qt.IsNil) 20 | c.Logf("gold request") 21 | resp, err := clientRequest(client, serverEndpoint+"/gold") 22 | c.Assert(err, qt.IsNil) 23 | c.Assert(resp, qt.Equals, "all is golden") 24 | 25 | c.Logf("silver request") 26 | resp, err = clientRequest(client, serverEndpoint+"/silver") 27 | c.Assert(err, qt.IsNil) 28 | c.Assert(resp, qt.Equals, "every cloud has a silver lining") 29 | } 30 | 31 | func BenchmarkExample(b *testing.B) { 32 | c := qt.New(b) 33 | f := newFixture(c) 34 | client := newClient() 35 | serverEndpoint, err := serve(func(endpoint string) (http.Handler, error) { 36 | return targetService(endpoint, f.authEndpoint, f.authPublicKey) 37 | }) 38 | c.Assert(err, qt.IsNil) 39 | b.ResetTimer() 40 | for i := 0; i < b.N; i++ { 41 | resp, err := clientRequest(client, serverEndpoint+"/gold") 42 | c.Assert(err, qt.IsNil) 43 | c.Assert(resp, qt.Equals, "all is golden") 44 | } 45 | } 46 | 47 | type fixture struct { 48 | authEndpoint string 49 | authPublicKey *bakery.PublicKey 50 | } 51 | 52 | func newFixture(c *qt.C) *fixture { 53 | var f fixture 54 | key := bakery.MustGenerateKey() 55 | f.authPublicKey = &key.Public 56 | var err error 57 | f.authEndpoint, err = serve(func(endpoint string) (http.Handler, error) { 58 | return authService(endpoint, key) 59 | }) 60 | c.Assert(err, qt.IsNil) 61 | return &f 62 | } 63 | -------------------------------------------------------------------------------- /bakery/example/main.go: -------------------------------------------------------------------------------- 1 | // This example demonstrates three components: 2 | // 3 | // - A target service, representing a web server that 4 | // wishes to use macaroons for authorization. 5 | // It delegates authorization to a third-party 6 | // authorization server by adding third-party 7 | // caveats to macaroons that it sends to the user. 8 | // 9 | // - A client, representing a client wanting to make 10 | // requests to the server. 11 | // 12 | // - An authorization server. 13 | // 14 | // In a real system, these three components would 15 | // live on different machines; the client component 16 | // could also be a web browser. 17 | // (TODO: write javascript discharge gatherer) 18 | package main 19 | 20 | import ( 21 | "fmt" 22 | "log" 23 | "net" 24 | "net/http" 25 | 26 | "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" 27 | "github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery" 28 | ) 29 | 30 | var defaultHTTPClient = httpbakery.NewHTTPClient() 31 | 32 | func main() { 33 | key, err := bakery.GenerateKey() 34 | if err != nil { 35 | log.Fatalf("cannot generate auth service key pair: %v", err) 36 | } 37 | authPublicKey := &key.Public 38 | authEndpoint := mustServe(func(endpoint string) (http.Handler, error) { 39 | return authService(endpoint, key) 40 | }) 41 | serverEndpoint := mustServe(func(endpoint string) (http.Handler, error) { 42 | return targetService(endpoint, authEndpoint, authPublicKey) 43 | }) 44 | 45 | resp, err := clientRequest(newClient(), serverEndpoint+"/gold/") 46 | if err != nil { 47 | log.Fatalf("client failed: %v", err) 48 | } 49 | fmt.Printf("client success: %q\n", resp) 50 | 51 | resp, err = clientRequest(newClient(), serverEndpoint+"/silver/") 52 | if err != nil { 53 | log.Fatalf("client failed: %v", err) 54 | } 55 | fmt.Printf("client success: %q\n", resp) 56 | } 57 | 58 | func mustServe(newHandler func(string) (http.Handler, error)) (endpointURL string) { 59 | endpoint, err := serve(newHandler) 60 | if err != nil { 61 | log.Fatalf("cannot serve: %v", err) 62 | } 63 | return endpoint 64 | } 65 | 66 | func serve(newHandler func(string) (http.Handler, error)) (endpointURL string, err error) { 67 | listener, err := net.Listen("tcp", "localhost:0") 68 | if err != nil { 69 | return "", fmt.Errorf("cannot listen: %v", err) 70 | } 71 | endpointURL = "http://" + listener.Addr().String() 72 | handler, err := newHandler(endpointURL) 73 | if err != nil { 74 | return "", fmt.Errorf("cannot start handler: %v", err) 75 | } 76 | go http.Serve(listener, handler) 77 | return endpointURL, nil 78 | } 79 | 80 | func newClient() *httpbakery.Client { 81 | c := httpbakery.NewClient() 82 | c.AddInteractor(httpbakery.WebBrowserInteractor{}) 83 | return c 84 | } 85 | -------------------------------------------------------------------------------- /bakery/example/targetservice.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | "strings" 8 | 9 | "gopkg.in/errgo.v1" 10 | 11 | "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" 12 | "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/checkers" 13 | "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/identchecker" 14 | "github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery" 15 | ) 16 | 17 | type targetServiceHandler struct { 18 | checker *identchecker.Checker 19 | oven *httpbakery.Oven 20 | authEndpoint string 21 | endpoint string 22 | mux *http.ServeMux 23 | } 24 | 25 | // targetService implements a "target service", representing 26 | // an arbitrary web service that wants to delegate authorization 27 | // to third parties. 28 | // 29 | func targetService(endpoint, authEndpoint string, authPK *bakery.PublicKey) (http.Handler, error) { 30 | key, err := bakery.GenerateKey() 31 | if err != nil { 32 | return nil, err 33 | } 34 | pkLocator := httpbakery.NewThirdPartyLocator(nil, nil) 35 | pkLocator.AllowInsecure() 36 | b := identchecker.NewBakery(identchecker.BakeryParams{ 37 | Key: key, 38 | Location: endpoint, 39 | Locator: pkLocator, 40 | Checker: httpbakery.NewChecker(), 41 | Authorizer: authorizer{ 42 | thirdPartyLocation: authEndpoint, 43 | }, 44 | }) 45 | mux := http.NewServeMux() 46 | srv := &targetServiceHandler{ 47 | checker: b.Checker, 48 | oven: &httpbakery.Oven{Oven: b.Oven}, 49 | authEndpoint: authEndpoint, 50 | } 51 | mux.Handle("/gold/", srv.auth(http.HandlerFunc(srv.serveGold))) 52 | mux.Handle("/silver/", srv.auth(http.HandlerFunc(srv.serveSilver))) 53 | return mux, nil 54 | } 55 | 56 | func (srv *targetServiceHandler) serveGold(w http.ResponseWriter, req *http.Request) { 57 | fmt.Fprintf(w, "all is golden") 58 | } 59 | 60 | func (srv *targetServiceHandler) serveSilver(w http.ResponseWriter, req *http.Request) { 61 | fmt.Fprintf(w, "every cloud has a silver lining") 62 | } 63 | 64 | // auth wraps the given handler with a handler that provides 65 | // authorization by inspecting the HTTP request 66 | // to decide what authorization is required. 67 | func (srv *targetServiceHandler) auth(h http.Handler) http.Handler { 68 | return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 69 | ctx := httpbakery.ContextWithRequest(context.TODO(), req) 70 | ops, err := opsForRequest(req) 71 | if err != nil { 72 | fail(w, http.StatusInternalServerError, "%v", err) 73 | return 74 | } 75 | authChecker := srv.checker.Auth(httpbakery.RequestMacaroons(req)...) 76 | if _, err = authChecker.Allow(ctx, ops...); err != nil { 77 | httpbakery.WriteError(ctx, w, srv.oven.Error(ctx, req, err)) 78 | return 79 | } 80 | h.ServeHTTP(w, req) 81 | }) 82 | } 83 | 84 | // opsForRequest returns the required operations 85 | // implied by the given HTTP request. 86 | func opsForRequest(req *http.Request) ([]bakery.Op, error) { 87 | if !strings.HasPrefix(req.URL.Path, "/") { 88 | return nil, errgo.Newf("bad path") 89 | } 90 | elems := strings.Split(req.URL.Path, "/") 91 | if len(elems) < 2 { 92 | return nil, errgo.Newf("bad path") 93 | } 94 | return []bakery.Op{{ 95 | Entity: elems[1], 96 | Action: req.Method, 97 | }}, nil 98 | } 99 | 100 | func fail(w http.ResponseWriter, code int, msg string, args ...interface{}) { 101 | http.Error(w, fmt.Sprintf(msg, args...), code) 102 | } 103 | 104 | type authorizer struct { 105 | thirdPartyLocation string 106 | } 107 | 108 | // Authorize implements bakery.Authorizer.Authorize by 109 | // allowing anyone to do anything if a third party 110 | // approves it. 111 | func (a authorizer) Authorize(ctx context.Context, id identchecker.Identity, ops []bakery.Op) (allowed []bool, caveats []checkers.Caveat, err error) { 112 | allowed = make([]bool, len(ops)) 113 | for i := range allowed { 114 | allowed[i] = true 115 | } 116 | caveats = []checkers.Caveat{{ 117 | Location: a.thirdPartyLocation, 118 | Condition: "access-allowed", 119 | }} 120 | return 121 | } 122 | -------------------------------------------------------------------------------- /bakery/export_test.go: -------------------------------------------------------------------------------- 1 | package bakery 2 | 3 | func SetMacaroonCaveatIdPrefix(m *Macaroon, prefix []byte) { 4 | m.caveatIdPrefix = prefix 5 | } 6 | 7 | func MacaroonCaveatData(m *Macaroon) map[string][]byte { 8 | return m.caveatData 9 | } 10 | 11 | var LegacyNamespace = legacyNamespace 12 | 13 | type MacaroonJSON macaroonJSON 14 | -------------------------------------------------------------------------------- /bakery/identchecker/authorizer.go: -------------------------------------------------------------------------------- 1 | package identchecker 2 | 3 | import ( 4 | "context" 5 | 6 | "gopkg.in/errgo.v1" 7 | 8 | "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" 9 | "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/checkers" 10 | ) 11 | 12 | // Authorizer is used to check whether a given user is allowed 13 | // to perform a set of operations. 14 | type Authorizer interface { 15 | // Authorize checks whether the given identity (which will be nil 16 | // when there is no authenticated user) is allowed to perform 17 | // the given operations. It should return an error only when 18 | // the authorization cannot be determined, not when the 19 | // user has been denied access. 20 | // 21 | // On success, each element of allowed holds whether the respective 22 | // element of ops has been allowed, and caveats holds any additional 23 | // third party caveats that apply. 24 | // If allowed is shorter then ops, the additional elements are assumed to 25 | // be false. 26 | Authorize(ctx context.Context, id Identity, ops []bakery.Op) (allowed []bool, caveats []checkers.Caveat, err error) 27 | } 28 | 29 | var ( 30 | // OpenAuthorizer is an Authorizer implementation that will authorize all operations without question. 31 | OpenAuthorizer openAuthorizer 32 | 33 | // ClosedAuthorizer is an Authorizer implementation that will return ErrPermissionDenied 34 | // on all authorization requests. 35 | ClosedAuthorizer closedAuthorizer 36 | ) 37 | 38 | var ( 39 | _ Authorizer = OpenAuthorizer 40 | _ Authorizer = ClosedAuthorizer 41 | _ Authorizer = AuthorizerFunc(nil) 42 | _ Authorizer = ACLAuthorizer{} 43 | ) 44 | 45 | type openAuthorizer struct{} 46 | 47 | // Authorize implements Authorizer.Authorize. 48 | func (openAuthorizer) Authorize(ctx context.Context, id Identity, ops []bakery.Op) (allowed []bool, caveats []checkers.Caveat, err error) { 49 | allowed = make([]bool, len(ops)) 50 | for i := range allowed { 51 | allowed[i] = true 52 | } 53 | return allowed, nil, nil 54 | } 55 | 56 | type closedAuthorizer struct{} 57 | 58 | // Authorize implements Authorizer.Authorize. 59 | func (closedAuthorizer) Authorize(ctx context.Context, id Identity, ops []bakery.Op) (allowed []bool, caveats []checkers.Caveat, err error) { 60 | return make([]bool, len(ops)), nil, nil 61 | } 62 | 63 | // AuthorizerFunc implements a simplified version of Authorizer 64 | // that operates on a single operation at a time. 65 | type AuthorizerFunc func(ctx context.Context, id Identity, op bakery.Op) (bool, []checkers.Caveat, error) 66 | 67 | // Authorize implements Authorizer.Authorize by calling f 68 | // with the given identity for each operation. 69 | func (f AuthorizerFunc) Authorize(ctx context.Context, id Identity, ops []bakery.Op) (allowed []bool, caveats []checkers.Caveat, err error) { 70 | allowed = make([]bool, len(ops)) 71 | for i, op := range ops { 72 | ok, fcaveats, err := f(ctx, id, op) 73 | if err != nil { 74 | return nil, nil, errgo.Mask(err) 75 | } 76 | allowed[i] = ok 77 | // TODO merge identical caveats? 78 | caveats = append(caveats, fcaveats...) 79 | } 80 | return allowed, caveats, nil 81 | } 82 | 83 | // Everyone is recognized by ACLAuthorizer as the name of a 84 | // group that has everyone in it. 85 | const Everyone = "everyone" 86 | 87 | // ACLIdentity may be implemented by Identity implementions 88 | // to report group membership information. 89 | // See ACLAuthorizer for details. 90 | type ACLIdentity interface { 91 | Identity 92 | 93 | // Allow reports whether the user should be allowed to access 94 | // any of the users or groups in the given ACL slice. 95 | Allow(ctx context.Context, acl []string) (bool, error) 96 | } 97 | 98 | // ACLAuthorizer is an Authorizer implementation that will check access 99 | // control list (ACL) membership of users. It uses GetACL to find out 100 | // the ACLs that apply to the requested operations and will authorize an 101 | // operation if an ACL contains the group "everyone" or if the context 102 | // contains an AuthInfo (see ContextWithAuthInfo) that holds an Identity 103 | // that implements ACLIdentity and its Allow method returns true for the 104 | // ACL. 105 | type ACLAuthorizer struct { 106 | // GetACL returns the ACL that applies to the given operation, 107 | // and reports whether non-authenticated users should 108 | // be allowed access when the ACL contains "everyone". 109 | // 110 | // If an entity cannot be found or the action is not recognised, 111 | // GetACLs should return an empty ACL but no error. 112 | GetACL func(ctx context.Context, op bakery.Op) (acl []string, allowPublic bool, err error) 113 | } 114 | 115 | // Authorize implements Authorizer.Authorize by calling ident.Allow to determine 116 | // whether the identity is a member of the ACLs associated with the given 117 | // operations. 118 | func (a ACLAuthorizer) Authorize(ctx context.Context, ident Identity, ops []bakery.Op) (allowed []bool, caveats []checkers.Caveat, err error) { 119 | if len(ops) == 0 { 120 | // Anyone is allowed to do nothing. 121 | return nil, nil, nil 122 | } 123 | ident1, _ := ident.(ACLIdentity) 124 | allowed = make([]bool, len(ops)) 125 | for i, op := range ops { 126 | acl, allowPublic, err := a.GetACL(ctx, op) 127 | if err != nil { 128 | return nil, nil, errgo.Mask(err) 129 | } 130 | if ident1 != nil { 131 | allowed[i], err = ident1.Allow(ctx, acl) 132 | if err != nil { 133 | return nil, nil, errgo.Notef(err, "cannot check permissions") 134 | } 135 | } else { 136 | // TODO should we allow "everyone" when the identity is 137 | // non-nil but isn't an ACLIdentity? 138 | allowed[i] = allowPublic && isPublicACL(acl) 139 | } 140 | } 141 | return allowed, nil, nil 142 | } 143 | 144 | func isPublicACL(acl []string) bool { 145 | for _, g := range acl { 146 | if g == Everyone { 147 | return true 148 | } 149 | } 150 | return false 151 | } 152 | -------------------------------------------------------------------------------- /bakery/identchecker/authorizer_test.go: -------------------------------------------------------------------------------- 1 | package identchecker_test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | qt "github.com/frankban/quicktest" 8 | "gopkg.in/errgo.v1" 9 | 10 | "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" 11 | "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/checkers" 12 | "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/identchecker" 13 | ) 14 | 15 | func TestAuthorizerFunc(t *testing.T) { 16 | c := qt.New(t) 17 | f := func(ctx context.Context, id identchecker.Identity, op bakery.Op) (bool, []checkers.Caveat, error) { 18 | c.Assert(ctx, qt.Equals, testContext) 19 | c.Assert(id, qt.Equals, identchecker.SimpleIdentity("bob")) 20 | switch op.Entity { 21 | case "a": 22 | return false, nil, nil 23 | case "b": 24 | return true, nil, nil 25 | case "c": 26 | return true, []checkers.Caveat{{ 27 | Location: "somewhere", 28 | Condition: "c", 29 | }}, nil 30 | case "d": 31 | return true, []checkers.Caveat{{ 32 | Location: "somewhere", 33 | Condition: "d", 34 | }}, nil 35 | } 36 | c.Fatalf("unexpected entity: %q", op.Entity) 37 | return false, nil, nil 38 | } 39 | allowed, caveats, err := identchecker.AuthorizerFunc(f).Authorize(testContext, identchecker.SimpleIdentity("bob"), []bakery.Op{{"a", "x"}, {"b", "x"}, {"c", "x"}, {"d", "x"}}) 40 | c.Assert(err, qt.IsNil) 41 | c.Assert(allowed, qt.DeepEquals, []bool{false, true, true, true}) 42 | c.Assert(caveats, qt.DeepEquals, []checkers.Caveat{{ 43 | Location: "somewhere", 44 | Condition: "c", 45 | }, { 46 | Location: "somewhere", 47 | Condition: "d", 48 | }}) 49 | } 50 | 51 | var aclAuthorizerTests = []struct { 52 | about string 53 | auth identchecker.ACLAuthorizer 54 | identity identchecker.Identity 55 | ops []bakery.Op 56 | expectAllowed []bool 57 | expectError string 58 | }{{ 59 | about: "no ops, no problem", 60 | auth: identchecker.ACLAuthorizer{ 61 | GetACL: func(ctx context.Context, op bakery.Op) ([]string, bool, error) { 62 | return nil, false, nil 63 | }, 64 | }, 65 | }, { 66 | about: "identity that does not implement ACLIdentity; user should be denied except for everyone group", 67 | auth: identchecker.ACLAuthorizer{ 68 | GetACL: func(ctx context.Context, op bakery.Op) ([]string, bool, error) { 69 | if op.Entity == "a" { 70 | return []string{identchecker.Everyone}, true, nil 71 | } else { 72 | return []string{"alice"}, false, nil 73 | } 74 | }, 75 | }, 76 | identity: simplestIdentity("bob"), 77 | ops: []bakery.Op{{ 78 | Entity: "a", 79 | Action: "a", 80 | }, { 81 | Entity: "b", 82 | Action: "b", 83 | }}, 84 | expectAllowed: []bool{true, false}, 85 | }, { 86 | about: "identity that does not implement ACLIdentity with user == Id; user should be denied except for everyone group", 87 | auth: identchecker.ACLAuthorizer{ 88 | GetACL: func(ctx context.Context, op bakery.Op) ([]string, bool, error) { 89 | if op.Entity == "a" { 90 | return []string{identchecker.Everyone}, true, nil 91 | } else { 92 | return []string{"bob"}, false, nil 93 | } 94 | }, 95 | }, 96 | identity: simplestIdentity("bob"), 97 | ops: []bakery.Op{{ 98 | Entity: "a", 99 | Action: "a", 100 | }, { 101 | Entity: "b", 102 | Action: "b", 103 | }}, 104 | expectAllowed: []bool{true, false}, 105 | }, { 106 | about: "permission denied for everyone without allow-public", 107 | auth: identchecker.ACLAuthorizer{ 108 | GetACL: func(ctx context.Context, op bakery.Op) ([]string, bool, error) { 109 | return []string{identchecker.Everyone}, false, nil 110 | }, 111 | }, 112 | identity: simplestIdentity("bob"), 113 | ops: []bakery.Op{{ 114 | Entity: "a", 115 | Action: "a", 116 | }}, 117 | expectAllowed: []bool{false}, 118 | }, { 119 | about: "permission granted to anyone with no identity with allow-public", 120 | auth: identchecker.ACLAuthorizer{ 121 | GetACL: func(ctx context.Context, op bakery.Op) ([]string, bool, error) { 122 | return []string{identchecker.Everyone}, true, nil 123 | }, 124 | }, 125 | ops: []bakery.Op{{ 126 | Entity: "a", 127 | Action: "a", 128 | }}, 129 | expectAllowed: []bool{true}, 130 | }, { 131 | about: "error return causes all authorization to fail", 132 | auth: identchecker.ACLAuthorizer{ 133 | GetACL: func(ctx context.Context, op bakery.Op) ([]string, bool, error) { 134 | if op.Entity == "a" { 135 | return []string{identchecker.Everyone}, true, nil 136 | } else { 137 | return nil, false, errgo.New("some error") 138 | } 139 | }, 140 | }, 141 | ops: []bakery.Op{{ 142 | Entity: "a", 143 | Action: "a", 144 | }, { 145 | Entity: "b", 146 | Action: "b", 147 | }}, 148 | expectError: "some error", 149 | }} 150 | 151 | func TestACLAuthorizer(t *testing.T) { 152 | c := qt.New(t) 153 | for i, test := range aclAuthorizerTests { 154 | c.Logf("test %d: %v", i, test.about) 155 | allowed, caveats, err := test.auth.Authorize(context.Background(), test.identity, test.ops) 156 | if test.expectError != "" { 157 | c.Assert(err, qt.ErrorMatches, test.expectError) 158 | c.Assert(allowed, qt.IsNil) 159 | c.Assert(caveats, qt.IsNil) 160 | continue 161 | } 162 | c.Assert(err, qt.IsNil) 163 | c.Assert(caveats, qt.IsNil) 164 | c.Assert(allowed, qt.DeepEquals, test.expectAllowed) 165 | } 166 | } 167 | 168 | // simplestIdentity implements Identity for a string. Unlike 169 | // simpleIdentity, it does not implement ACLIdentity. 170 | type simplestIdentity string 171 | 172 | func (id simplestIdentity) Id() string { 173 | return string(id) 174 | } 175 | 176 | func (simplestIdentity) Domain() string { 177 | return "" 178 | } 179 | -------------------------------------------------------------------------------- /bakery/identchecker/bakery.go: -------------------------------------------------------------------------------- 1 | package identchecker 2 | 3 | import ( 4 | "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" 5 | "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/checkers" 6 | ) 7 | 8 | type Bakery struct { 9 | Oven *bakery.Oven 10 | Checker *Checker 11 | } 12 | 13 | // BakeryParams holds a selection of parameters for the Oven 14 | // and the Checker created by New. 15 | // 16 | // For more fine-grained control of parameters, create the 17 | // Oven or Checker directly. 18 | // 19 | // The zero value is OK to use, but won't allow any authentication 20 | // or third party caveats to be added. 21 | type BakeryParams struct { 22 | // Checker holds the checker used to check first party caveats. 23 | // If this is nil, New will use checkers.New(nil). 24 | Checker bakery.FirstPartyCaveatChecker 25 | 26 | // RootKeyStore holds the root key store to use. If you need to 27 | // use a different root key store for different operations, 28 | // you'll need to pass a RootKeyStoreForOps value to NewOven 29 | // directly. 30 | // 31 | // If this is nil, New will use NewMemRootKeyStore(). 32 | // Note that that is almost certain insufficient for production services 33 | // that are spread across multiple instances or that need 34 | // to persist keys across restarts. 35 | RootKeyStore bakery.RootKeyStore 36 | 37 | // Locator is used to find out information on third parties when 38 | // adding third party caveats. If this is nil, no non-local third 39 | // party caveats can be added. 40 | Locator bakery.ThirdPartyLocator 41 | 42 | // Key holds the private key of the oven. If this is nil, 43 | // no third party caveats may be added. 44 | Key *bakery.KeyPair 45 | 46 | // IdentityClient holds the identity implementation to use for 47 | // authentication. If this is nil, no authentication will be possible. 48 | IdentityClient IdentityClient 49 | 50 | // Authorizer is used to check whether an authenticated user is 51 | // allowed to perform operations. If it is nil, New will 52 | // use ClosedAuthorizer. 53 | // 54 | // The identity parameter passed to Authorizer.Allow will 55 | // always have been obtained from a call to 56 | // IdentityClient.DeclaredIdentity. 57 | Authorizer Authorizer 58 | 59 | // Location holds the location to use when creating new macaroons. 60 | Location string 61 | 62 | // Logger is used to log checker operations. If it is nil, 63 | // DefaultLogger("bakery.identchecker") will be used. 64 | Logger bakery.Logger 65 | } 66 | 67 | // NewBakery returns a new Bakery instance which combines an Oven with a 68 | // Checker for the convenience of callers that wish to use both 69 | // together. 70 | func NewBakery(p BakeryParams) *Bakery { 71 | if p.Checker == nil { 72 | p.Checker = checkers.New(nil) 73 | } 74 | ovenParams := bakery.OvenParams{ 75 | Key: p.Key, 76 | Namespace: p.Checker.Namespace(), 77 | Location: p.Location, 78 | Locator: p.Locator, 79 | LegacyMacaroonOp: LoginOp, 80 | } 81 | if p.RootKeyStore != nil { 82 | ovenParams.RootKeyStoreForOps = func(ops []bakery.Op) bakery.RootKeyStore { 83 | return p.RootKeyStore 84 | } 85 | } 86 | oven := bakery.NewOven(ovenParams) 87 | 88 | checker := NewChecker(CheckerParams{ 89 | Checker: p.Checker, 90 | MacaroonVerifier: oven, 91 | IdentityClient: p.IdentityClient, 92 | Authorizer: p.Authorizer, 93 | Logger: p.Logger, 94 | }) 95 | return &Bakery{ 96 | Oven: oven, 97 | Checker: checker, 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /bakery/identchecker/common_test.go: -------------------------------------------------------------------------------- 1 | package identchecker_test 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "time" 7 | 8 | "gopkg.in/macaroon.v2" 9 | 10 | "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" 11 | "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/checkers" 12 | ) 13 | 14 | // testContext holds the testing background context - its associated time when checking 15 | // time-before caveats will always be the value of epoch. 16 | var testContext = checkers.ContextWithClock(context.Background(), stoppedClock{epoch}) 17 | 18 | var ( 19 | epoch = time.Date(1900, 11, 17, 19, 00, 13, 0, time.UTC) 20 | ) 21 | 22 | var testChecker = func() *checkers.Checker { 23 | c := checkers.New(nil) 24 | c.Namespace().Register("testns", "") 25 | c.Register("true", "testns", trueCheck) 26 | return c 27 | }() 28 | 29 | // trueCheck always succeeds. 30 | func trueCheck(ctx context.Context, cond, args string) error { 31 | return nil 32 | } 33 | 34 | func macStr(m *macaroon.Macaroon) string { 35 | data, err := json.MarshalIndent(m, "\t", "\t") 36 | if err != nil { 37 | panic(err) 38 | } 39 | return string(data) 40 | } 41 | 42 | type stoppedClock struct { 43 | t time.Time 44 | } 45 | 46 | func (t stoppedClock) Now() time.Time { 47 | return t.t 48 | } 49 | 50 | type basicAuthKey struct{} 51 | 52 | type basicAuth struct { 53 | user, password string 54 | } 55 | 56 | func contextWithBasicAuth(ctx context.Context, user, password string) context.Context { 57 | return context.WithValue(ctx, basicAuthKey{}, basicAuth{user, password}) 58 | } 59 | 60 | func basicAuthFromContext(ctx context.Context) (user, password string) { 61 | auth, _ := ctx.Value(basicAuthKey{}).(basicAuth) 62 | return auth.user, auth.password 63 | } 64 | 65 | func mustGenerateKey() *bakery.KeyPair { 66 | return bakery.MustGenerateKey() 67 | } 68 | -------------------------------------------------------------------------------- /bakery/identchecker/identity.go: -------------------------------------------------------------------------------- 1 | package identchecker 2 | 3 | import ( 4 | "context" 5 | 6 | "gopkg.in/errgo.v1" 7 | 8 | "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/checkers" 9 | ) 10 | 11 | // IdentityClient represents an abstract identity manager. User 12 | // identities can be based on local informaton (for example 13 | // HTTP basic auth) or by reference to an external trusted 14 | // third party (an identity manager). 15 | type IdentityClient interface { 16 | // IdentityFromContext returns the identity based on information in the context. 17 | // If it cannot determine the identity based on the context, then it 18 | // should return a set of caveats containing a third party caveat that, 19 | // when discharged, can be used to obtain the identity with DeclaredIdentity. 20 | // 21 | // It should only return an error if it cannot check the identity 22 | // (for example because of a database access error) - it's 23 | // OK to return all zero values when there's 24 | // no identity found and no third party to address caveats to. 25 | IdentityFromContext(ctx context.Context) (Identity, []checkers.Caveat, error) 26 | 27 | // DeclaredIdentity parses the identity declaration from the given 28 | // declared attributes. 29 | // TODO take the set of first party caveat conditions instead? 30 | DeclaredIdentity(ctx context.Context, declared map[string]string) (Identity, error) 31 | } 32 | 33 | // Identity holds identity information declared in a first party caveat 34 | // added when discharging a third party caveat. 35 | type Identity interface { 36 | // Id returns the id of the user, which may be an 37 | // opaque blob with no human meaning. 38 | // An id is only considered to be unique 39 | // with a given domain. 40 | Id() string 41 | 42 | // Domain holds the domain of the user. This 43 | // will be empty if the user was authenticated 44 | // directly with the identity provider. 45 | Domain() string 46 | } 47 | 48 | // noIdentities defines the null identity provider - it never returns any identities. 49 | type noIdentities struct{} 50 | 51 | // IdentityFromContext implements IdentityClient.IdentityFromContext by 52 | // never returning a declared identity or any caveats. 53 | func (noIdentities) IdentityFromContext(ctx context.Context) (Identity, []checkers.Caveat, error) { 54 | return nil, nil, nil 55 | } 56 | 57 | // DeclaredIdentity implements IdentityClient.DeclaredIdentity by 58 | // always returning an error. 59 | func (noIdentities) DeclaredIdentity(ctx context.Context, declared map[string]string) (Identity, error) { 60 | return nil, errgo.Newf("no identity declared or possible") 61 | } 62 | 63 | var _ ACLIdentity = SimpleIdentity("") 64 | 65 | // SimpleIdentity implements a simple form of identity where 66 | // the user is represented by a string. 67 | type SimpleIdentity string 68 | 69 | // Domain implements Identity.Domain by always 70 | // returning the empty domain. 71 | func (SimpleIdentity) Domain() string { 72 | return "" 73 | } 74 | 75 | // Id returns id as a string. 76 | func (id SimpleIdentity) Id() string { 77 | return string(id) 78 | } 79 | 80 | // Allow implements ACLIdentity by allowing the identity access to 81 | // ACL members that are equal to id. That is, some user u is considered 82 | // a member of group u and no other. 83 | func (id SimpleIdentity) Allow(ctx context.Context, acl []string) (bool, error) { 84 | for _, g := range acl { 85 | if string(id) == g { 86 | return true, nil 87 | } 88 | } 89 | return false, nil 90 | } 91 | -------------------------------------------------------------------------------- /bakery/keys.go: -------------------------------------------------------------------------------- 1 | package bakery 2 | 3 | import ( 4 | "context" 5 | "crypto/rand" 6 | "encoding/base64" 7 | "encoding/json" 8 | "strings" 9 | "sync" 10 | 11 | "golang.org/x/crypto/curve25519" 12 | "golang.org/x/crypto/nacl/box" 13 | "gopkg.in/errgo.v1" 14 | "gopkg.in/macaroon.v2" 15 | ) 16 | 17 | // KeyLen is the byte length of the Ed25519 public and private keys used for 18 | // caveat id encryption. 19 | const KeyLen = 32 20 | 21 | // NonceLen is the byte length of the nonce values used for caveat id 22 | // encryption. 23 | const NonceLen = 24 24 | 25 | // PublicKey is a 256-bit Ed25519 public key. 26 | type PublicKey struct { 27 | Key 28 | } 29 | 30 | // PrivateKey is a 256-bit Ed25519 private key. 31 | type PrivateKey struct { 32 | Key 33 | } 34 | 35 | // Public derives the public key from a private key. 36 | func (k PrivateKey) Public() PublicKey { 37 | var pub PublicKey 38 | curve25519.ScalarBaseMult((*[32]byte)(&pub.Key), (*[32]byte)(&k.Key)) 39 | return pub 40 | } 41 | 42 | // Key is a 256-bit Ed25519 key. 43 | type Key [KeyLen]byte 44 | 45 | // String returns the base64 representation of the key. 46 | func (k Key) String() string { 47 | return base64.StdEncoding.EncodeToString(k[:]) 48 | } 49 | 50 | // MarshalBinary implements encoding.BinaryMarshaler.MarshalBinary. 51 | func (k Key) MarshalBinary() ([]byte, error) { 52 | return k[:], nil 53 | } 54 | 55 | // isZero reports whether the key consists entirely of zeros. 56 | func (k Key) isZero() bool { 57 | return k == Key{} 58 | } 59 | 60 | // UnmarshalBinary implements encoding.BinaryUnmarshaler.UnmarshalBinary. 61 | func (k *Key) UnmarshalBinary(data []byte) error { 62 | if len(data) != len(k) { 63 | return errgo.Newf("wrong length for key, got %d want %d", len(data), len(k)) 64 | } 65 | copy(k[:], data) 66 | return nil 67 | } 68 | 69 | // MarshalText implements encoding.TextMarshaler.MarshalText. 70 | func (k Key) MarshalText() ([]byte, error) { 71 | data := make([]byte, base64.StdEncoding.EncodedLen(len(k))) 72 | base64.StdEncoding.Encode(data, k[:]) 73 | return data, nil 74 | } 75 | 76 | // boxKey returns the box package's type for a key. 77 | func (k Key) boxKey() *[KeyLen]byte { 78 | return (*[KeyLen]byte)(&k) 79 | } 80 | 81 | // UnmarshalText implements encoding.TextUnmarshaler.UnmarshalText. 82 | func (k *Key) UnmarshalText(text []byte) error { 83 | data, err := macaroon.Base64Decode(text) 84 | if err != nil { 85 | return errgo.Notef(err, "cannot decode base64 key") 86 | } 87 | if len(data) != len(k) { 88 | return errgo.Newf("wrong length for key, got %d want %d", len(data), len(k)) 89 | } 90 | copy(k[:], data) 91 | return nil 92 | } 93 | 94 | // ThirdPartyInfo holds information on a given third party 95 | // discharge service. 96 | type ThirdPartyInfo struct { 97 | // PublicKey holds the public key of the third party. 98 | PublicKey PublicKey 99 | 100 | // Version holds latest the bakery protocol version supported 101 | // by the discharger. 102 | Version Version 103 | } 104 | 105 | // ThirdPartyLocator is used to find information on third 106 | // party discharge services. 107 | type ThirdPartyLocator interface { 108 | // ThirdPartyInfo returns information on the third 109 | // party at the given location. It returns ErrNotFound if no match is found. 110 | // This method must be safe to call concurrently. 111 | ThirdPartyInfo(ctx context.Context, loc string) (ThirdPartyInfo, error) 112 | } 113 | 114 | // ThirdPartyStore implements a simple ThirdPartyLocator. 115 | // A trailing slash on locations is ignored. 116 | type ThirdPartyStore struct { 117 | mu sync.RWMutex 118 | m map[string]ThirdPartyInfo 119 | } 120 | 121 | // NewThirdPartyStore returns a new instance of ThirdPartyStore 122 | // that stores locations in memory. 123 | func NewThirdPartyStore() *ThirdPartyStore { 124 | return &ThirdPartyStore{ 125 | m: make(map[string]ThirdPartyInfo), 126 | } 127 | } 128 | 129 | // AddInfo associates the given information with the 130 | // given location, ignoring any trailing slash. 131 | // This method is OK to call concurrently with sThirdPartyInfo. 132 | func (s *ThirdPartyStore) AddInfo(loc string, info ThirdPartyInfo) { 133 | s.mu.Lock() 134 | defer s.mu.Unlock() 135 | s.m[canonicalLocation(loc)] = info 136 | } 137 | 138 | func canonicalLocation(loc string) string { 139 | return strings.TrimSuffix(loc, "/") 140 | } 141 | 142 | // ThirdPartyInfo implements the ThirdPartyLocator interface. 143 | func (s *ThirdPartyStore) ThirdPartyInfo(ctx context.Context, loc string) (ThirdPartyInfo, error) { 144 | s.mu.RLock() 145 | defer s.mu.RUnlock() 146 | if info, ok := s.m[canonicalLocation(loc)]; ok { 147 | return info, nil 148 | } 149 | return ThirdPartyInfo{}, ErrNotFound 150 | } 151 | 152 | // KeyPair holds a public/private pair of keys. 153 | type KeyPair struct { 154 | Public PublicKey `json:"public"` 155 | Private PrivateKey `json:"private"` 156 | } 157 | 158 | // UnmarshalJSON implements json.Unmarshaler. 159 | func (k *KeyPair) UnmarshalJSON(data []byte) error { 160 | type keyPair KeyPair 161 | if err := json.Unmarshal(data, (*keyPair)(k)); err != nil { 162 | return err 163 | } 164 | return k.validate() 165 | } 166 | 167 | // UnmarshalYAML implements yaml.Unmarshaler. 168 | func (k *KeyPair) UnmarshalYAML(unmarshal func(interface{}) error) error { 169 | type keyPair KeyPair 170 | if err := unmarshal((*keyPair)(k)); err != nil { 171 | return err 172 | } 173 | return k.validate() 174 | } 175 | 176 | func (k *KeyPair) validate() error { 177 | if k.Public.isZero() { 178 | return errgo.Newf("missing public key") 179 | } 180 | if k.Private.isZero() { 181 | return errgo.Newf("missing private key") 182 | } 183 | return nil 184 | } 185 | 186 | // GenerateKey generates a new key pair. 187 | func GenerateKey() (*KeyPair, error) { 188 | var key KeyPair 189 | pub, priv, err := box.GenerateKey(rand.Reader) 190 | if err != nil { 191 | return nil, err 192 | } 193 | key.Public = PublicKey{*pub} 194 | key.Private = PrivateKey{*priv} 195 | return &key, nil 196 | } 197 | 198 | // MustGenerateKey is like GenerateKey but panics if GenerateKey returns 199 | // an error - useful in tests. 200 | func MustGenerateKey() *KeyPair { 201 | key, err := GenerateKey() 202 | if err != nil { 203 | panic(errgo.Notef(err, "cannot generate key")) 204 | } 205 | return key 206 | } 207 | 208 | // String implements the fmt.Stringer interface 209 | // by returning the base64 representation of the 210 | // public key part of key. 211 | func (key *KeyPair) String() string { 212 | return key.Public.String() 213 | } 214 | 215 | type emptyLocator struct{} 216 | 217 | func (emptyLocator) ThirdPartyInfo(context.Context, string) (ThirdPartyInfo, error) { 218 | return ThirdPartyInfo{}, ErrNotFound 219 | } 220 | -------------------------------------------------------------------------------- /bakery/keys_test.go: -------------------------------------------------------------------------------- 1 | package bakery_test 2 | 3 | import ( 4 | "encoding/base64" 5 | "encoding/json" 6 | "testing" 7 | 8 | qt "github.com/frankban/quicktest" 9 | "gopkg.in/yaml.v2" 10 | 11 | "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" 12 | ) 13 | 14 | var testKey = newTestKey(0) 15 | 16 | func TestMarshalBinary(t *testing.T) { 17 | c := qt.New(t) 18 | data, err := testKey.MarshalBinary() 19 | c.Assert(err, qt.IsNil) 20 | c.Assert(data, qt.DeepEquals, []byte(testKey[:])) 21 | 22 | var key1 bakery.Key 23 | err = key1.UnmarshalBinary(data) 24 | c.Assert(err, qt.IsNil) 25 | c.Assert(key1, qt.DeepEquals, testKey) 26 | } 27 | 28 | func TestMarshalText(t *testing.T) { 29 | c := qt.New(t) 30 | data, err := testKey.MarshalText() 31 | c.Assert(err, qt.IsNil) 32 | c.Assert(string(data), qt.Equals, base64.StdEncoding.EncodeToString([]byte(testKey[:]))) 33 | 34 | var key1 bakery.Key 35 | err = key1.UnmarshalText(data) 36 | c.Assert(err, qt.IsNil) 37 | c.Assert(key1, qt.Equals, testKey) 38 | } 39 | 40 | func TestUnmarshalTextWrongKeyLength(t *testing.T) { 41 | c := qt.New(t) 42 | var key bakery.Key 43 | err := key.UnmarshalText([]byte("aGVsbG8K")) 44 | c.Assert(err, qt.ErrorMatches, `wrong length for key, got 6 want 32`) 45 | } 46 | 47 | func TestKeyPairMarshalJSON(t *testing.T) { 48 | c := qt.New(t) 49 | kp := bakery.KeyPair{ 50 | Public: bakery.PublicKey{testKey}, 51 | Private: bakery.PrivateKey{testKey}, 52 | } 53 | kp.Private.Key[0] = 99 54 | data, err := json.Marshal(kp) 55 | c.Assert(err, qt.IsNil) 56 | var x map[string]interface{} 57 | err = json.Unmarshal(data, &x) 58 | c.Assert(err, qt.IsNil) 59 | 60 | // Check that the fields have marshaled as strings. 61 | _, ok := x["private"].(string) 62 | c.Assert(ok, qt.Equals, true) 63 | _, ok = x["public"].(string) 64 | c.Assert(ok, qt.Equals, true) 65 | 66 | var kp1 bakery.KeyPair 67 | err = json.Unmarshal(data, &kp1) 68 | c.Assert(err, qt.IsNil) 69 | c.Assert(kp1, qt.DeepEquals, kp) 70 | } 71 | 72 | func TestKeyPairMarshalYAML(t *testing.T) { 73 | c := qt.New(t) 74 | kp := bakery.KeyPair{ 75 | Public: bakery.PublicKey{testKey}, 76 | Private: bakery.PrivateKey{testKey}, 77 | } 78 | kp.Private.Key[0] = 99 79 | data, err := yaml.Marshal(kp) 80 | c.Assert(err, qt.IsNil) 81 | var x map[string]interface{} 82 | err = yaml.Unmarshal(data, &x) 83 | c.Assert(err, qt.IsNil) 84 | 85 | // Check that the fields have marshaled as strings. 86 | _, ok := x["private"].(string) 87 | c.Assert(ok, qt.Equals, true) 88 | _, ok = x["public"].(string) 89 | c.Assert(ok, qt.Equals, true) 90 | 91 | var kp1 bakery.KeyPair 92 | err = yaml.Unmarshal(data, &kp1) 93 | c.Assert(err, qt.IsNil) 94 | c.Assert(kp1, qt.DeepEquals, kp) 95 | } 96 | 97 | func TestKeyPairUnmarshalJSONMissingPublicKey(t *testing.T) { 98 | c := qt.New(t) 99 | data := `{"private": "7ZcOvDAW9opAIPzJ7FdSbz2i2qL8bFZapDlmNLpMzpU="}` 100 | var k bakery.KeyPair 101 | err := json.Unmarshal([]byte(data), &k) 102 | c.Assert(err, qt.ErrorMatches, `missing public key`) 103 | } 104 | 105 | func TestKeyPairUnmarshalJSONMissingPrivateKey(t *testing.T) { 106 | c := qt.New(t) 107 | data := `{"public": "7ZcOvDAW9opAIPzJ7FdSbz2i2qL8bFZapDlmNLpMzpU="}` 108 | var k bakery.KeyPair 109 | err := json.Unmarshal([]byte(data), &k) 110 | c.Assert(err, qt.ErrorMatches, `missing private key`) 111 | } 112 | 113 | func TestKeyPairUnmarshalJSONEmptyKeys(t *testing.T) { 114 | c := qt.New(t) 115 | data := `{"private": "", "public": ""}` 116 | var k bakery.KeyPair 117 | err := json.Unmarshal([]byte(data), &k) 118 | c.Assert(err, qt.ErrorMatches, `wrong length for key, got 0 want 32`) 119 | } 120 | 121 | func TestKeyPairUnmarshalJSONNoKeys(t *testing.T) { 122 | c := qt.New(t) 123 | data := `{}` 124 | var k bakery.KeyPair 125 | err := json.Unmarshal([]byte(data), &k) 126 | c.Assert(err, qt.ErrorMatches, `missing public key`) 127 | } 128 | 129 | func TestKeyPairUnmarshalYAMLMissingPublicKey(t *testing.T) { 130 | c := qt.New(t) 131 | data := ` 132 | private: 7ZcOvDAW9opAIPzJ7FdSbz2i2qL8bFZapDlmNLpMzpU= 133 | ` 134 | var k bakery.KeyPair 135 | err := yaml.Unmarshal([]byte(data), &k) 136 | c.Assert(err, qt.ErrorMatches, `missing public key`) 137 | } 138 | 139 | func TestKeyPairUnmarshalYAMLMissingPrivateKey(t *testing.T) { 140 | c := qt.New(t) 141 | data := ` 142 | public: 7ZcOvDAW9opAIPzJ7FdSbz2i2qL8bFZapDlmNLpMzpU= 143 | ` 144 | var k bakery.KeyPair 145 | err := yaml.Unmarshal([]byte(data), &k) 146 | c.Assert(err, qt.ErrorMatches, `missing private key`) 147 | } 148 | 149 | func TestDerivePublicFromPrivate(t *testing.T) { 150 | c := qt.New(t) 151 | k := mustGenerateKey() 152 | c.Assert(k.Private.Public(), qt.Equals, k.Public) 153 | } 154 | 155 | func newTestKey(n byte) bakery.Key { 156 | var k bakery.Key 157 | for i := range k { 158 | k[i] = n + byte(i) 159 | } 160 | return k 161 | } 162 | -------------------------------------------------------------------------------- /bakery/logger.go: -------------------------------------------------------------------------------- 1 | package bakery 2 | 3 | import ( 4 | "context" 5 | ) 6 | 7 | // Logger is used by the bakery to log informational messages 8 | // about bakery operations. 9 | type Logger interface { 10 | Infof(ctx context.Context, f string, args ...interface{}) 11 | Debugf(ctx context.Context, f string, args ...interface{}) 12 | } 13 | 14 | // DefaultLogger returns a Logger instance that does nothing. 15 | // 16 | // Deprecated: DefaultLogger exists for historical compatibility 17 | // only. Previously it logged using github.com/juju/loggo. 18 | func DefaultLogger(name string) Logger { 19 | return nopLogger{} 20 | } 21 | 22 | type nopLogger struct{} 23 | 24 | // Debugf implements Logger.Debugf. 25 | func (nopLogger) Debugf(context.Context, string, ...interface{}) {} 26 | 27 | // Debugf implements Logger.Infof. 28 | func (nopLogger) Infof(context.Context, string, ...interface{}) {} 29 | -------------------------------------------------------------------------------- /bakery/mgorootkeystore/export_test.go: -------------------------------------------------------------------------------- 1 | package mgorootkeystore 2 | 3 | var ( 4 | Clock = &clock 5 | MgoCollectionFindId = &mgoCollectionFindId 6 | ) 7 | -------------------------------------------------------------------------------- /bakery/oven_test.go: -------------------------------------------------------------------------------- 1 | package bakery_test 2 | 3 | import ( 4 | "testing" 5 | 6 | qt "github.com/frankban/quicktest" 7 | "gopkg.in/macaroon.v2" 8 | 9 | "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" 10 | ) 11 | 12 | var canonicalOpsTests = []struct { 13 | about string 14 | ops []bakery.Op 15 | expect []bakery.Op 16 | }{{ 17 | about: "empty slice", 18 | }, { 19 | about: "one element", 20 | ops: []bakery.Op{{"a", "a"}}, 21 | expect: []bakery.Op{{"a", "a"}}, 22 | }, { 23 | about: "all in order", 24 | ops: []bakery.Op{{"a", "a"}, {"a", "b"}, {"c", "c"}}, 25 | expect: []bakery.Op{{"a", "a"}, {"a", "b"}, {"c", "c"}}, 26 | }, { 27 | about: "out of order", 28 | ops: []bakery.Op{{"c", "c"}, {"a", "b"}, {"a", "a"}}, 29 | expect: []bakery.Op{{"a", "a"}, {"a", "b"}, {"c", "c"}}, 30 | }, { 31 | about: "with duplicates", 32 | ops: []bakery.Op{{"c", "c"}, {"a", "b"}, {"a", "a"}, {"c", "a"}, {"c", "b"}, {"c", "c"}, {"a", "a"}}, 33 | expect: []bakery.Op{{"a", "a"}, {"a", "b"}, {"c", "a"}, {"c", "b"}, {"c", "c"}}, 34 | }, { 35 | about: "make sure we've got the fields right", 36 | ops: []bakery.Op{{Entity: "read", Action: "two"}, {Entity: "read", Action: "one"}, {Entity: "write", Action: "one"}}, 37 | expect: []bakery.Op{{Entity: "read", Action: "one"}, {Entity: "read", Action: "two"}, {Entity: "write", Action: "one"}}, 38 | }} 39 | 40 | func TestCanonicalOps(t *testing.T) { 41 | c := qt.New(t) 42 | for i, test := range canonicalOpsTests { 43 | c.Logf("test %d: %v", i, test.about) 44 | ops := append([]bakery.Op(nil), test.ops...) 45 | c.Assert(bakery.CanonicalOps(ops), qt.DeepEquals, test.expect) 46 | // Verify that the original slice isn't changed. 47 | c.Assert(ops, qt.DeepEquals, test.ops) 48 | } 49 | } 50 | 51 | func TestMultipleOps(t *testing.T) { 52 | c := qt.New(t) 53 | oven := bakery.NewOven(bakery.OvenParams{}) 54 | ops := []bakery.Op{{"one", "read"}, {"one", "write"}, {"two", "read"}} 55 | m, err := oven.NewMacaroon(testContext, bakery.LatestVersion, nil, ops...) 56 | c.Assert(err, qt.IsNil) 57 | gotOps, conds, err := oven.VerifyMacaroon(testContext, macaroon.Slice{m.M()}) 58 | c.Assert(err, qt.IsNil) 59 | c.Assert(conds, qt.HasLen, 0) 60 | c.Assert(bakery.CanonicalOps(gotOps), qt.DeepEquals, ops) 61 | } 62 | 63 | func TestMultipleOpsInId(t *testing.T) { 64 | c := qt.New(t) 65 | oven := bakery.NewOven(bakery.OvenParams{}) 66 | 67 | ops := []bakery.Op{{"one", "read"}, {"one", "write"}, {"two", "read"}} 68 | m, err := oven.NewMacaroon(testContext, bakery.LatestVersion, nil, ops...) 69 | c.Assert(err, qt.IsNil) 70 | gotOps, conds, err := oven.VerifyMacaroon(testContext, macaroon.Slice{m.M()}) 71 | c.Assert(err, qt.IsNil) 72 | c.Assert(conds, qt.HasLen, 0) 73 | c.Assert(bakery.CanonicalOps(gotOps), qt.DeepEquals, ops) 74 | } 75 | 76 | func TestMultipleOpsInIdWithVersion1(t *testing.T) { 77 | c := qt.New(t) 78 | oven := bakery.NewOven(bakery.OvenParams{}) 79 | 80 | ops := []bakery.Op{{"one", "read"}, {"one", "write"}, {"two", "read"}} 81 | m, err := oven.NewMacaroon(testContext, bakery.Version1, nil, ops...) 82 | c.Assert(err, qt.IsNil) 83 | gotOps, conds, err := oven.VerifyMacaroon(testContext, macaroon.Slice{m.M()}) 84 | c.Assert(err, qt.IsNil) 85 | c.Assert(conds, qt.HasLen, 0) 86 | c.Assert(bakery.CanonicalOps(gotOps), qt.DeepEquals, ops) 87 | } 88 | -------------------------------------------------------------------------------- /bakery/postgresrootkeystore/export_test.go: -------------------------------------------------------------------------------- 1 | package postgresrootkeystore 2 | 3 | import "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/dbrootkeystore" 4 | 5 | var ( 6 | Clock = &clock 7 | NewBacking = &newBacking 8 | ) 9 | 10 | func Backing(keys *RootKeys) dbrootkeystore.Backing { 11 | return backing{keys} 12 | } 13 | -------------------------------------------------------------------------------- /bakery/postgresrootkeystore/rootkey.go: -------------------------------------------------------------------------------- 1 | // Package postgreskeystore provides an implementation of bakery.RootKeyStore 2 | // that uses Postgres as a persistent store. 3 | package postgresrootkeystore 4 | 5 | import ( 6 | "database/sql" 7 | "sync" 8 | "time" 9 | 10 | "gopkg.in/errgo.v1" 11 | 12 | "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" 13 | "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/dbrootkeystore" 14 | ) 15 | 16 | // Variables defined so they can be overidden for testing. 17 | var ( 18 | clock dbrootkeystore.Clock 19 | newBacking = func(s *RootKeys) dbrootkeystore.Backing { 20 | return backing{s} 21 | } 22 | ) 23 | 24 | // TODO it would be nice if could make Policy 25 | // a type alias for dbrootkeystore.Policy, 26 | // but we want to be able to support versions 27 | // of Go from before type aliases were introduced. 28 | 29 | // Policy holds a store policy for root keys. 30 | type Policy dbrootkeystore.Policy 31 | 32 | // maxPolicyCache holds the maximum number of store policies that can 33 | // hold cached keys in a given RootKeys instance. 34 | // 35 | // 100 is probably overkill, given that practical systems will 36 | // likely only have a small number of active policies on any given 37 | // macaroon collection. 38 | const maxPolicyCache = 100 39 | 40 | // RootKeys represents a cache of macaroon root keys. 41 | type RootKeys struct { 42 | keys *dbrootkeystore.RootKeys 43 | 44 | db *sql.DB 45 | table string 46 | stmts [numStmts]*sql.Stmt 47 | 48 | // initDBOnce guards initDBErr. 49 | initDBOnce sync.Once 50 | initDBErr error 51 | } 52 | 53 | // NewRootKeys returns a root-keys cache that 54 | // uses the given table in the given Postgres database for storage 55 | // and is limited in size to approximately the given size. 56 | // The table will be created lazily when the root key store 57 | // is first used. 58 | // 59 | // The returned RootKeys instance must be closed after use. 60 | // 61 | // It also creates other SQL resources using the table name 62 | // as a prefix. 63 | // 64 | // Use the NewStore method to obtain a RootKeyStore 65 | // implementation suitable for particular root key 66 | // lifetimes. 67 | func NewRootKeys(db *sql.DB, table string, maxCacheSize int) *RootKeys { 68 | return &RootKeys{ 69 | keys: dbrootkeystore.NewRootKeys(maxCacheSize, clock), 70 | db: db, 71 | table: table, 72 | } 73 | } 74 | 75 | // Close closes the RootKeys instance. This must be called after using the instance. 76 | func (s *RootKeys) Close() error { 77 | var retErr error 78 | for _, stmt := range s.stmts { 79 | if stmt == nil { 80 | continue 81 | } 82 | if err := stmt.Close(); err != nil && retErr == nil { 83 | retErr = err 84 | } 85 | } 86 | return errgo.Mask(retErr) 87 | } 88 | 89 | // NewStore returns a new RootKeyStore implementation that 90 | // stores and obtains root keys from the given collection. 91 | // 92 | // Root keys will be generated and stored following the 93 | // given store policy. 94 | // 95 | // It is expected that all collections passed to a given Store's 96 | // NewStore method should refer to the same underlying collection. 97 | func (s *RootKeys) NewStore(policy Policy) bakery.RootKeyStore { 98 | b := newBacking(s) 99 | return s.keys.NewStore(b, dbrootkeystore.Policy(policy)) 100 | } 101 | 102 | // backing implements dbrootkeystore.Backing by using Postgres as 103 | // a backing store. 104 | type backing struct { 105 | keys *RootKeys 106 | } 107 | 108 | // GetKey implements dbrootkeystore.Backing.GetKey. 109 | func (b backing) GetKey(id []byte) (dbrootkeystore.RootKey, error) { 110 | return b.keys.getKey(id) 111 | } 112 | 113 | // InsertKey implements dbrootkeystore.Backing.InsertKey. 114 | func (b backing) InsertKey(key dbrootkeystore.RootKey) error { 115 | return b.keys.insertKey(key) 116 | } 117 | 118 | // FindLatestKey implements dbrootkeystore.Backing.FindLatestKey. 119 | func (b backing) FindLatestKey(createdAfter, expiresAfter, expiresBefore time.Time) (dbrootkeystore.RootKey, error) { 120 | return b.keys.findLatestKey(createdAfter, expiresAfter, expiresBefore) 121 | } 122 | -------------------------------------------------------------------------------- /bakery/postgresrootkeystore/sql.go: -------------------------------------------------------------------------------- 1 | package postgresrootkeystore 2 | 3 | import ( 4 | "bytes" 5 | "database/sql" 6 | "fmt" 7 | "text/template" 8 | "time" 9 | 10 | "gopkg.in/errgo.v1" 11 | 12 | "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" 13 | "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/dbrootkeystore" 14 | ) 15 | 16 | type stmtId int 17 | 18 | const ( 19 | findIdStmt stmtId = iota 20 | findBestRootKeyStmt 21 | insertKeyStmt 22 | numStmts 23 | ) 24 | 25 | var initStatements = ` 26 | BEGIN; 27 | 28 | -- Set up an advisory lock so that only one thread can issue the statements 29 | -- below at a time, to avoid issues cause by concurrent updates (especially in 30 | -- the 'CREATE OR REPLACE FUNCTION' statement). 31 | -- The lock value is random, and it should be shared by all callers of this 32 | -- script. It is automatically released on commit. 33 | SELECT pg_advisory_xact_lock(34577509137); 34 | 35 | CREATE TABLE IF NOT EXISTS {{.Table}} ( 36 | id BYTEA PRIMARY KEY NOT NULL, 37 | rootkey BYTEA, 38 | created TIMESTAMP WITH TIME ZONE NOT NULL, 39 | expires TIMESTAMP WITH TIME ZONE NOT NULL 40 | ); 41 | 42 | CREATE OR REPLACE FUNCTION {{.ExpireFunc}}() RETURNS trigger 43 | LANGUAGE plpgsql 44 | AS $$ 45 | BEGIN 46 | DELETE FROM {{.Table}} WHERE expires < NOW(); 47 | RETURN NEW; 48 | END; 49 | $$; 50 | 51 | CREATE INDEX IF NOT EXISTS {{.CreateIndex}} ON {{.Table}} (created); 52 | 53 | CREATE INDEX IF NOT EXISTS {{.ExpireIndex}} ON {{.Table}} (expires); 54 | 55 | DROP TRIGGER IF EXISTS {{.ExpireTrigger}} ON {{.Table}}; 56 | 57 | CREATE TRIGGER {{.ExpireTrigger}} 58 | BEFORE INSERT ON {{.Table}} 59 | EXECUTE PROCEDURE {{.ExpireFunc}}(); 60 | 61 | COMMIT; 62 | ` 63 | 64 | type templateParams struct { 65 | Table string 66 | ExpireFunc string 67 | CreateIndex string 68 | ExpireIndex string 69 | ExpireTrigger string 70 | } 71 | 72 | func (s *RootKeys) initDB() error { 73 | s.initDBOnce.Do(func() { 74 | s.initDBErr = s._initDB() 75 | }) 76 | if s.initDBErr != nil { 77 | return errgo.Notef(s.initDBErr, "cannot initialize database") 78 | } 79 | return nil 80 | } 81 | 82 | func (s *RootKeys) _initDB() error { 83 | p := &templateParams{ 84 | Table: s.table, 85 | ExpireFunc: s.table + "_expire_func", 86 | CreateIndex: s.table + "_index_create", 87 | ExpireIndex: s.table + "_index_expire", 88 | ExpireTrigger: s.table + "_trigger", 89 | } 90 | if _, err := s.db.Exec(templateVal(p, initStatements)); err != nil { 91 | return errgo.Notef(err, "cannot initialize table") 92 | } 93 | if err := s.prepareAll(p); err != nil { 94 | return errgo.Notef(err, "cannot prepare statements") 95 | } 96 | return nil 97 | } 98 | 99 | func (s *RootKeys) prepareAll(p *templateParams) error { 100 | if err := s.prepareFindId(p); err != nil { 101 | return errgo.Mask(err) 102 | } 103 | if err := s.prepareFindBestRootKey(p); err != nil { 104 | return errgo.Mask(err) 105 | } 106 | if err := s.prepareInsertKey(p); err != nil { 107 | return errgo.Mask(err) 108 | } 109 | return nil 110 | } 111 | 112 | func (s *RootKeys) prepareFindId(p *templateParams) error { 113 | return s.prepare(findIdStmt, p, ` 114 | SELECT id, created, expires, rootkey FROM {{.Table}} WHERE id=$1 115 | `) 116 | } 117 | 118 | func (s *RootKeys) getKey(id []byte) (dbrootkeystore.RootKey, error) { 119 | if err := s.initDB(); err != nil { 120 | return dbrootkeystore.RootKey{}, errgo.Mask(err) 121 | } 122 | var key dbrootkeystore.RootKey 123 | err := s.stmts[findIdStmt].QueryRow(id).Scan( 124 | &key.Id, 125 | &key.Created, 126 | &key.Expires, 127 | &key.RootKey, 128 | ) 129 | switch { 130 | case err == sql.ErrNoRows: 131 | return dbrootkeystore.RootKey{}, bakery.ErrNotFound 132 | case err != nil: 133 | return dbrootkeystore.RootKey{}, errgo.Mask(err) 134 | } 135 | return key, nil 136 | } 137 | 138 | func (s *RootKeys) prepareFindBestRootKey(p *templateParams) error { 139 | return s.prepare(findBestRootKeyStmt, p, ` 140 | SELECT id, created, expires, rootkey FROM {{.Table}} 141 | WHERE 142 | created >= $1 AND 143 | expires >= $2 AND 144 | expires <= $3 145 | ORDER BY created DESC 146 | `) 147 | } 148 | 149 | func (s *RootKeys) findLatestKey(createdAfter, expiresAfter, expiresBefore time.Time) (dbrootkeystore.RootKey, error) { 150 | if err := s.initDB(); err != nil { 151 | return dbrootkeystore.RootKey{}, errgo.Mask(err) 152 | } 153 | var key dbrootkeystore.RootKey 154 | err := s.stmts[findBestRootKeyStmt].QueryRow( 155 | createdAfter, 156 | expiresAfter, 157 | expiresBefore, 158 | ).Scan( 159 | &key.Id, 160 | &key.Created, 161 | &key.Expires, 162 | &key.RootKey, 163 | ) 164 | if err == sql.ErrNoRows || err == nil { 165 | return key, nil 166 | } 167 | return dbrootkeystore.RootKey{}, errgo.Mask(err) 168 | } 169 | 170 | func (s *RootKeys) prepareInsertKey(p *templateParams) error { 171 | return s.prepare(insertKeyStmt, p, ` 172 | INSERT into {{.Table}} (id, rootkey, created, expires) VALUES ($1, $2, $3, $4) 173 | `) 174 | } 175 | 176 | func (s *RootKeys) insertKey(key dbrootkeystore.RootKey) error { 177 | if err := s.initDB(); err != nil { 178 | return errgo.Mask(err) 179 | } 180 | _, err := s.stmts[insertKeyStmt].Exec(key.Id, key.RootKey, key.Created, key.Expires) 181 | return errgo.Mask(err) 182 | } 183 | 184 | func (s *RootKeys) prepare(id stmtId, p *templateParams, tmpl string) error { 185 | if s.stmts[id] != nil { 186 | panic(fmt.Sprintf("statement %v prepared twice", id)) 187 | } 188 | stmt, err := s.db.Prepare(templateVal(p, tmpl)) 189 | if err != nil { 190 | return errgo.Notef(err, "statement %v (%q) invalid", id, templateVal(p, tmpl)) 191 | } 192 | s.stmts[id] = stmt 193 | return nil 194 | } 195 | 196 | func templateVal(p *templateParams, s string) string { 197 | tmpl := template.Must(template.New("").Parse(s)) 198 | var buf bytes.Buffer 199 | if err := tmpl.Execute(&buf, p); err != nil { 200 | panic(errgo.Notef(err, "cannot create initialization statements")) 201 | } 202 | return buf.String() 203 | } 204 | -------------------------------------------------------------------------------- /bakery/slice.go: -------------------------------------------------------------------------------- 1 | package bakery 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | 8 | "gopkg.in/errgo.v1" 9 | "gopkg.in/macaroon.v2" 10 | 11 | "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/checkers" 12 | ) 13 | 14 | // Slice holds a slice of unbound macaroons. 15 | type Slice []*Macaroon 16 | 17 | // Bind prepares the macaroon slice for use in a request. This must be 18 | // done before presenting the macaroons to a service for use as 19 | // authorization tokens. The result will only be valid 20 | // if s contains discharge macaroons for all third party 21 | // caveats. 22 | // 23 | // All the macaroons in the returned slice will be copies 24 | // of this in s, not references. 25 | func (s Slice) Bind() macaroon.Slice { 26 | if len(s) == 0 { 27 | return nil 28 | } 29 | ms := make(macaroon.Slice, len(s)) 30 | ms[0] = s[0].M().Clone() 31 | rootSig := ms[0].Signature() 32 | for i, m := range s[1:] { 33 | m1 := m.M().Clone() 34 | m1.Bind(rootSig) 35 | ms[i+1] = m1 36 | } 37 | return ms 38 | } 39 | 40 | // Purge returns a new slice holding all macaroons in s 41 | // that expire after the given time. 42 | func (ms Slice) Purge(t time.Time) Slice { 43 | ms1 := make(Slice, 0, len(ms)) 44 | for i, m := range ms { 45 | et, ok := checkers.ExpiryTime(m.Namespace(), m.M().Caveats()) 46 | if !ok || et.After(t) { 47 | ms1 = append(ms1, m) 48 | } else if i == 0 { 49 | // The primary macaroon has expired, so all its discharges 50 | // have expired too. 51 | // TODO purge all discharge macaroons when the macaroon 52 | // containing their third-party caveat expires. 53 | return nil 54 | } 55 | } 56 | return ms1 57 | } 58 | 59 | // DischargeAll discharges all the third party caveats in the slice for 60 | // which discharge macaroons are not already present, using getDischarge 61 | // to acquire the discharge macaroons. It always returns the slice with 62 | // any acquired discharge macaroons added, even on error. It returns an 63 | // error if all the discharges could not be acquired. 64 | // 65 | // Note that this differs from DischargeAll in that it can be given several existing 66 | // discharges, and that the resulting discharges are not bound to the primary, 67 | // so it's still possible to add caveats and reacquire expired discharges 68 | // without reacquiring the primary macaroon. 69 | func (ms Slice) DischargeAll(ctx context.Context, getDischarge func(ctx context.Context, cav macaroon.Caveat, encryptedCaveat []byte) (*Macaroon, error), localKey *KeyPair) (Slice, error) { 70 | if len(ms) == 0 { 71 | return nil, errgo.Newf("no macaroons to discharge") 72 | } 73 | ms1 := make(Slice, len(ms)) 74 | copy(ms1, ms) 75 | // have holds the keys of all the macaroon ids in the slice. 76 | type needCaveat struct { 77 | // cav holds the caveat that needs discharge. 78 | cav macaroon.Caveat 79 | // encryptedCaveat holds encrypted caveat 80 | // if it was held externally. 81 | encryptedCaveat []byte 82 | } 83 | var need []needCaveat 84 | have := make(map[string]bool) 85 | for _, m := range ms[1:] { 86 | have[string(m.M().Id())] = true 87 | } 88 | // addCaveats adds any required third party caveats to the need slice 89 | // that aren't already present . 90 | addCaveats := func(m *Macaroon) { 91 | for _, cav := range m.M().Caveats() { 92 | if len(cav.VerificationId) == 0 || have[string(cav.Id)] { 93 | continue 94 | } 95 | need = append(need, needCaveat{ 96 | cav: cav, 97 | encryptedCaveat: m.caveatData[string(cav.Id)], 98 | }) 99 | } 100 | } 101 | for _, m := range ms { 102 | addCaveats(m) 103 | } 104 | var errs []error 105 | for len(need) > 0 { 106 | cav := need[0] 107 | need = need[1:] 108 | var dm *Macaroon 109 | var err error 110 | if localKey != nil && cav.cav.Location == "local" { 111 | // TODO use a small caveat id. 112 | dm, err = Discharge(ctx, DischargeParams{ 113 | Key: localKey, 114 | Checker: localDischargeChecker, 115 | Caveat: cav.encryptedCaveat, 116 | Id: cav.cav.Id, 117 | Locator: emptyLocator{}, 118 | }) 119 | } else { 120 | dm, err = getDischarge(ctx, cav.cav, cav.encryptedCaveat) 121 | } 122 | if err != nil { 123 | errs = append(errs, errgo.NoteMask(err, fmt.Sprintf("cannot get discharge from %q", cav.cav.Location), errgo.Any)) 124 | continue 125 | } 126 | ms1 = append(ms1, dm) 127 | addCaveats(dm) 128 | } 129 | if errs != nil { 130 | // TODO log other errors? Return them all? 131 | return ms1, errgo.Mask(errs[0], errgo.Any) 132 | } 133 | return ms1, nil 134 | } 135 | -------------------------------------------------------------------------------- /bakery/slice_test.go: -------------------------------------------------------------------------------- 1 | package bakery_test 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "testing" 7 | "time" 8 | 9 | qt "github.com/frankban/quicktest" 10 | "gopkg.in/errgo.v1" 11 | "gopkg.in/macaroon.v2" 12 | 13 | "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" 14 | "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/checkers" 15 | ) 16 | 17 | func TestAddMoreCaveats(t *testing.T) { 18 | c := qt.New(t) 19 | getDischarge := func(_ context.Context, cav macaroon.Caveat, payload []byte) (*bakery.Macaroon, error) { 20 | c.Check(payload, qt.IsNil) 21 | m, err := bakery.NewMacaroon([]byte("root key "+string(cav.Id)), cav.Id, "", bakery.LatestVersion, nil) 22 | c.Assert(err, qt.Equals, nil) 23 | return m, nil 24 | } 25 | 26 | rootKey := []byte("root key") 27 | m, err := bakery.NewMacaroon(rootKey, []byte("id0"), "loc0", bakery.LatestVersion, testChecker.Namespace()) 28 | c.Assert(err, qt.Equals, nil) 29 | err = m.M().AddThirdPartyCaveat([]byte("root key id1"), []byte("id1"), "somewhere") 30 | c.Assert(err, qt.Equals, nil) 31 | 32 | ms, err := bakery.Slice{m}.DischargeAll(testContext, getDischarge, nil) 33 | c.Assert(err, qt.Equals, nil) 34 | c.Assert(ms, qt.HasLen, 2) 35 | 36 | mms := ms.Bind() 37 | c.Assert(mms, qt.HasLen, len(ms)) 38 | err = mms[0].Verify(rootKey, alwaysOK, mms[1:]) 39 | c.Assert(err, qt.Equals, nil) 40 | 41 | // Add another caveat and to the root macaroon and discharge it. 42 | err = ms[0].M().AddThirdPartyCaveat([]byte("root key id2"), []byte("id2"), "somewhere else") 43 | c.Assert(err, qt.Equals, nil) 44 | 45 | ms, err = ms.DischargeAll(testContext, getDischarge, nil) 46 | c.Assert(err, qt.Equals, nil) 47 | c.Assert(ms, qt.HasLen, 3) 48 | 49 | mms = ms.Bind() 50 | err = mms[0].Verify(rootKey, alwaysOK, mms[1:]) 51 | c.Assert(err, qt.Equals, nil) 52 | 53 | // Check that we can remove the original discharge and still re-acquire it OK. 54 | ms = bakery.Slice{ms[0], ms[2]} 55 | 56 | ms, err = ms.DischargeAll(testContext, getDischarge, nil) 57 | c.Assert(err, qt.Equals, nil) 58 | c.Assert(ms, qt.HasLen, 3) 59 | 60 | mms = ms.Bind() 61 | err = mms[0].Verify(rootKey, alwaysOK, mms[1:]) 62 | c.Assert(err, qt.Equals, nil) 63 | } 64 | 65 | func TestPurge(t *testing.T) { 66 | c := qt.New(t) 67 | t0 := time.Date(2000, time.October, 1, 12, 0, 0, 0, time.UTC) 68 | clock := &stoppedClock{ 69 | t: t0, 70 | } 71 | ctx := checkers.ContextWithClock(testContext, clock) 72 | checkCond := func(cond string) error { 73 | return testChecker.CheckFirstPartyCaveat(ctx, cond) 74 | } 75 | 76 | rootKey := []byte("root key") 77 | m, err := bakery.NewMacaroon(rootKey, []byte("id0"), "loc0", bakery.LatestVersion, testChecker.Namespace()) 78 | c.Assert(err, qt.Equals, nil) 79 | err = m.AddCaveat(ctx, checkers.TimeBeforeCaveat(t0.Add(time.Hour)), nil, nil) 80 | c.Assert(err, qt.Equals, nil) 81 | err = m.M().AddThirdPartyCaveat([]byte("root key id1"), []byte("id1"), "somewhere") 82 | c.Assert(err, qt.Equals, nil) 83 | ms := bakery.Slice{m} 84 | 85 | getDischarge := func(_ context.Context, cav macaroon.Caveat, payload []byte) (*bakery.Macaroon, error) { 86 | c.Check(payload, qt.IsNil) 87 | m, err := bakery.NewMacaroon([]byte("root key "+string(cav.Id)), cav.Id, "", bakery.LatestVersion, testChecker.Namespace()) 88 | c.Assert(err, qt.Equals, nil) 89 | err = m.AddCaveat(ctx, checkers.TimeBeforeCaveat(clock.t.Add(time.Minute)), nil, nil) 90 | c.Assert(err, qt.Equals, nil) 91 | return m, nil 92 | } 93 | ms, err = ms.DischargeAll(testContext, getDischarge, nil) 94 | c.Assert(err, qt.Equals, nil) 95 | c.Assert(ms, qt.HasLen, 2) 96 | 97 | mms := ms.Bind() 98 | err = mms[0].Verify(rootKey, checkCond, mms[1:]) 99 | c.Assert(err, qt.Equals, nil) 100 | 101 | // Sanity check that verification fails when the discharge time has expired. 102 | clock.t = t0.Add(2 * time.Minute) 103 | 104 | err = mms[0].Verify(rootKey, checkCond, mms[1:]) 105 | c.Assert(err, qt.ErrorMatches, `.*: macaroon has expired`) 106 | 107 | // Purge removes the discharge macaroon when it's out of date. 108 | ms = ms.Purge(clock.t) 109 | c.Assert(ms, qt.HasLen, 1) 110 | 111 | // Reacquire a discharge macaroon. 112 | ms, err = ms.DischargeAll(testContext, getDischarge, nil) 113 | c.Assert(err, qt.Equals, nil) 114 | c.Assert(ms, qt.HasLen, 2) 115 | 116 | // The macaroons should now be valid again. 117 | mms = ms.Bind() 118 | err = mms[0].Verify(rootKey, checkCond, mms[1:]) 119 | c.Assert(err, qt.Equals, nil) 120 | 121 | // Check that when the time has gone beyond the primary 122 | // macaroon's expiry time, Purge removes all the macaroons. 123 | 124 | // Reacquire a discharge macaroon just before the primary 125 | // macaroon's expiry time. 126 | clock.t = t0.Add(time.Hour - time.Second) 127 | 128 | ms = ms.Purge(clock.t) 129 | c.Assert(ms, qt.HasLen, 1) 130 | ms, err = ms.DischargeAll(testContext, getDischarge, nil) 131 | c.Assert(err, qt.Equals, nil) 132 | c.Assert(ms, qt.HasLen, 2) 133 | 134 | // The macaroons should now be valid again. 135 | mms = ms.Bind() 136 | err = mms[0].Verify(rootKey, checkCond, mms[1:]) 137 | c.Assert(err, qt.Equals, nil) 138 | 139 | // But once we've passed the hour, the primary expires 140 | // even though the discharge is valid, and purging 141 | // removes both primary and discharge. 142 | 143 | ms = ms.Purge(t0.Add(time.Hour + time.Millisecond)) 144 | c.Assert(ms, qt.HasLen, 0) 145 | } 146 | 147 | func TestDischargeAllAcquiresManyMacaroonsAsPossible(t *testing.T) { 148 | c := qt.New(t) 149 | failIds := map[string]bool{ 150 | "id1": true, 151 | "id3": true, 152 | } 153 | 154 | getDischarge := func(_ context.Context, cav macaroon.Caveat, payload []byte) (*bakery.Macaroon, error) { 155 | if failIds[string(cav.Id)] { 156 | return nil, errgo.Newf("discharge failure on %q", cav.Id) 157 | } 158 | m, err := bakery.NewMacaroon([]byte("root key "+string(cav.Id)), cav.Id, "", bakery.LatestVersion, nil) 159 | c.Assert(err, qt.Equals, nil) 160 | return m, nil 161 | } 162 | 163 | rootKey := []byte("root key") 164 | m, err := bakery.NewMacaroon(rootKey, []byte("id-root"), "", bakery.LatestVersion, testChecker.Namespace()) 165 | c.Assert(err, qt.Equals, nil) 166 | for i := 0; i < 5; i++ { 167 | id := fmt.Sprintf("id%d", i) 168 | err = m.M().AddThirdPartyCaveat([]byte("root key "+id), []byte(id), "somewhere") 169 | c.Assert(err, qt.Equals, nil) 170 | } 171 | ms := bakery.Slice{m} 172 | 173 | ms, err = ms.DischargeAll(testContext, getDischarge, nil) 174 | c.Check(err, qt.ErrorMatches, `cannot get discharge from "somewhere": discharge failure on "id1"`) 175 | c.Assert(ms, qt.HasLen, 4) 176 | 177 | // Try again without id1 failing - we should acquire one more discharge. 178 | // Mark the other ones as failing because we shouldn't be trying to acquire 179 | // them because they're already in the slice. 180 | failIds = map[string]bool{ 181 | "id0": true, 182 | "id3": true, 183 | "id4": true, 184 | } 185 | 186 | ms, err = ms.DischargeAll(testContext, getDischarge, nil) 187 | c.Check(err, qt.ErrorMatches, `cannot get discharge from "somewhere": discharge failure on "id3"`) 188 | c.Assert(ms, qt.HasLen, 5) 189 | 190 | failIds["id3"] = false 191 | 192 | ms, err = ms.DischargeAll(testContext, getDischarge, nil) 193 | c.Check(err, qt.Equals, nil) 194 | c.Assert(ms, qt.HasLen, 6) 195 | 196 | mms := ms.Bind() 197 | err = mms[0].Verify(rootKey, alwaysOK, mms[1:]) 198 | c.Assert(err, qt.Equals, nil) 199 | } 200 | -------------------------------------------------------------------------------- /bakery/store.go: -------------------------------------------------------------------------------- 1 | package bakery 2 | 3 | import ( 4 | "context" 5 | "sync" 6 | ) 7 | 8 | // RootKeyStore defines store for macaroon root keys. 9 | type RootKeyStore interface { 10 | // Get returns the root key for the given id. 11 | // If the item is not there, it returns ErrNotFound. 12 | Get(ctx context.Context, id []byte) ([]byte, error) 13 | 14 | // RootKey returns the root key to be used for making a new 15 | // macaroon, and an id that can be used to look it up later with 16 | // the Get method. 17 | // 18 | // Note that the root keys should remain available for as long 19 | // as the macaroons using them are valid. 20 | // 21 | // Note that there is no need for it to return a new root key 22 | // for every call - keys may be reused, although some key 23 | // cycling is over time is advisable. 24 | RootKey(ctx context.Context) (rootKey []byte, id []byte, err error) 25 | } 26 | 27 | // NewMemRootKeyStore returns an implementation of 28 | // Store that generates a single key and always 29 | // returns that from RootKey. The same id ("0") is always 30 | // used. 31 | func NewMemRootKeyStore() RootKeyStore { 32 | return new(memRootKeyStore) 33 | } 34 | 35 | type memRootKeyStore struct { 36 | mu sync.Mutex 37 | key []byte 38 | } 39 | 40 | // Get implements Store.Get. 41 | func (s *memRootKeyStore) Get(_ context.Context, id []byte) ([]byte, error) { 42 | s.mu.Lock() 43 | defer s.mu.Unlock() 44 | if len(id) != 1 || id[0] != '0' || s.key == nil { 45 | return nil, ErrNotFound 46 | } 47 | return s.key, nil 48 | } 49 | 50 | // RootKey implements Store.RootKey by always returning the same root 51 | // key. 52 | func (s *memRootKeyStore) RootKey(context.Context) (rootKey, id []byte, err error) { 53 | s.mu.Lock() 54 | defer s.mu.Unlock() 55 | if s.key == nil { 56 | newKey, err := randomBytes(24) 57 | if err != nil { 58 | return nil, nil, err 59 | } 60 | s.key = newKey 61 | } 62 | return s.key, []byte("0"), nil 63 | } 64 | -------------------------------------------------------------------------------- /bakery/store_test.go: -------------------------------------------------------------------------------- 1 | package bakery_test 2 | 3 | import ( 4 | "testing" 5 | 6 | qt "github.com/frankban/quicktest" 7 | 8 | "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" 9 | ) 10 | 11 | func TestMemStore(t *testing.T) { 12 | c := qt.New(t) 13 | store := bakery.NewMemRootKeyStore() 14 | key, err := store.Get(nil, []byte("x")) 15 | c.Assert(err, qt.Equals, bakery.ErrNotFound) 16 | c.Assert(key, qt.IsNil) 17 | 18 | key, err = store.Get(nil, []byte("0")) 19 | c.Assert(err, qt.Equals, bakery.ErrNotFound) 20 | c.Assert(key, qt.IsNil) 21 | 22 | key, id, err := store.RootKey(nil) 23 | c.Assert(err, qt.IsNil) 24 | c.Assert(key, qt.HasLen, 24) 25 | c.Assert(string(id), qt.Equals, "0") 26 | 27 | key1, id1, err := store.RootKey(nil) 28 | c.Assert(err, qt.IsNil) 29 | c.Assert(key1, qt.DeepEquals, key) 30 | c.Assert(id1, qt.DeepEquals, id) 31 | 32 | key2, err := store.Get(nil, id) 33 | c.Assert(err, qt.IsNil) 34 | c.Assert(key2, qt.DeepEquals, key) 35 | 36 | _, err = store.Get(nil, []byte("1")) 37 | c.Assert(err, qt.Equals, bakery.ErrNotFound) 38 | } 39 | -------------------------------------------------------------------------------- /bakery/version.go: -------------------------------------------------------------------------------- 1 | package bakery 2 | 3 | import "gopkg.in/macaroon.v2" 4 | 5 | // Version represents a version of the bakery protocol. 6 | type Version int 7 | 8 | const ( 9 | // In version 0, discharge-required errors use status 407 10 | Version0 Version = 0 11 | // In version 1, discharge-required errors use status 401. 12 | Version1 Version = 1 13 | // In version 2, binary macaroons and caveat ids are supported. 14 | Version2 Version = 2 15 | // In version 3, we support operations associated with macaroons 16 | // and external third party caveats. 17 | Version3 Version = 3 18 | LatestVersion = Version3 19 | ) 20 | 21 | // MacaroonVersion returns the macaroon version that should 22 | // be used with the given bakery Version. 23 | func MacaroonVersion(v Version) macaroon.Version { 24 | switch v { 25 | case Version0, Version1: 26 | return macaroon.V1 27 | default: 28 | return macaroon.V2 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /bakerytest/bakerytest.go: -------------------------------------------------------------------------------- 1 | // Package bakerytest provides test helper functions for 2 | // the bakery. 3 | package bakerytest 4 | 5 | import ( 6 | "context" 7 | "crypto/tls" 8 | "net/http" 9 | "net/http/httptest" 10 | "sync" 11 | 12 | "github.com/julienschmidt/httprouter" 13 | "gopkg.in/errgo.v1" 14 | "gopkg.in/httprequest.v1" 15 | 16 | "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" 17 | "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/checkers" 18 | "github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery" 19 | ) 20 | 21 | // Discharger represents a third party caveat discharger server. 22 | type Discharger struct { 23 | server *httptest.Server 24 | 25 | // Mux holds the HTTP multiplexor used by 26 | // the discharger server. 27 | Mux *httprouter.Router 28 | 29 | // Key holds the discharger's private key. 30 | Key *bakery.KeyPair 31 | 32 | // Locator holds the third party locator 33 | // used when adding a third party caveat 34 | // returned by a third party caveat checker. 35 | Locator bakery.ThirdPartyLocator 36 | 37 | // CheckerP is called to check third party caveats when they're 38 | // discharged. It defaults to NopThirdPartyCaveatCheckerP. 39 | CheckerP httpbakery.ThirdPartyCaveatCheckerP 40 | 41 | // Checker is the deprecated version of CheckerP, and will be 42 | // ignored if CheckerP is non-nil. 43 | Checker httpbakery.ThirdPartyCaveatChecker 44 | } 45 | 46 | // NewDischarger returns a new discharger server that can be used to 47 | // discharge third party caveats. It uses the given locator to add third 48 | // party caveats returned by the Checker. The discharger also acts as a 49 | // locator, returning locator information for itself only. 50 | // 51 | // The returned discharger should be closed after use. 52 | // 53 | // This should not be used concurrently unless httpbakery.AllowInsecureThirdPartyLocator 54 | // is set, because otherwise it needs to run a TLS server and modify http.DefaultTransport 55 | // to allow insecure connections. 56 | func NewDischarger(locator bakery.ThirdPartyLocator) *Discharger { 57 | key, err := bakery.GenerateKey() 58 | if err != nil { 59 | panic(err) 60 | } 61 | d := &Discharger{ 62 | Mux: httprouter.New(), 63 | Key: key, 64 | Locator: locator, 65 | } 66 | handler := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 67 | d.Mux.ServeHTTP(w, req) 68 | }) 69 | if httpbakery.AllowInsecureThirdPartyLocator { 70 | d.server = httptest.NewServer(handler) 71 | } else { 72 | d.server = httptest.NewTLSServer(handler) 73 | startSkipVerify() 74 | } 75 | bd := httpbakery.NewDischarger(httpbakery.DischargerParams{ 76 | Key: key, 77 | Locator: locator, 78 | CheckerP: d, 79 | }) 80 | d.AddHTTPHandlers(bd.Handlers()) 81 | return d 82 | } 83 | 84 | // AddHTTPHandlers adds the given HTTP handlers to the 85 | // set of endpoints handled by the discharger. 86 | func (d *Discharger) AddHTTPHandlers(hs []httprequest.Handler) { 87 | for _, h := range hs { 88 | d.Mux.Handle(h.Method, h.Path, h.Handle) 89 | } 90 | } 91 | 92 | // Close shuts down the server. It may be called more than 93 | // once on the same discharger. 94 | func (d *Discharger) Close() { 95 | if d.server == nil { 96 | return 97 | } 98 | d.server.Close() 99 | stopSkipVerify() 100 | d.server = nil 101 | } 102 | 103 | // Location returns the location of the discharger, suitable 104 | // for setting as the location in a third party caveat. 105 | // This will be the URL of the server. 106 | func (d *Discharger) Location() string { 107 | return d.server.URL 108 | } 109 | 110 | // PublicKeyForLocation implements bakery.PublicKeyLocator 111 | // by returning information on the discharger's server location 112 | // only. 113 | func (d *Discharger) ThirdPartyInfo(ctx context.Context, loc string) (bakery.ThirdPartyInfo, error) { 114 | if loc == d.Location() { 115 | return bakery.ThirdPartyInfo{ 116 | PublicKey: d.Key.Public, 117 | Version: bakery.LatestVersion, 118 | }, nil 119 | } 120 | return bakery.ThirdPartyInfo{}, bakery.ErrNotFound 121 | } 122 | 123 | // DischargeMacaroon returns a discharge macaroon 124 | // for the given caveat information with the given 125 | // caveats added. It assumed the actual third party 126 | // caveat has already been checked. 127 | func (d *Discharger) DischargeMacaroon( 128 | ctx context.Context, 129 | cav *bakery.ThirdPartyCaveatInfo, 130 | caveats []checkers.Caveat, 131 | ) (*bakery.Macaroon, error) { 132 | return bakery.Discharge(ctx, bakery.DischargeParams{ 133 | Id: cav.Id, 134 | Caveat: cav.Caveat, 135 | Key: d.Key, 136 | Checker: bakery.ThirdPartyCaveatCheckerFunc(func(ctx context.Context, cav *bakery.ThirdPartyCaveatInfo) ([]checkers.Caveat, error) { 137 | return caveats, nil 138 | }), 139 | Locator: d.Locator, 140 | }) 141 | } 142 | 143 | var ErrTokenNotRecognized = errgo.New("discharge token not recognized") 144 | 145 | // CheckThirdPartyCaveat implements httpbakery.ThirdPartyCaveatCheckerP 146 | // by calling d.CheckerP, or d.Checker if that's nil. 147 | func (d *Discharger) CheckThirdPartyCaveat(ctx context.Context, p httpbakery.ThirdPartyCaveatCheckerParams) ([]checkers.Caveat, error) { 148 | if d.CheckerP != nil { 149 | return d.CheckerP.CheckThirdPartyCaveat(ctx, p) 150 | } 151 | if d.Checker == nil { 152 | return nil, nil 153 | } 154 | return d.Checker.CheckThirdPartyCaveat(ctx, p.Caveat, p.Request, p.Token) 155 | } 156 | 157 | // ConditionParser adapts the given function into an httpbakery.ThirdPartyCaveatCheckerP. 158 | // It parses the caveat's condition and calls the function with the result. 159 | func ConditionParser(check func(cond, arg string) ([]checkers.Caveat, error)) httpbakery.ThirdPartyCaveatCheckerP { 160 | f := func(ctx context.Context, p httpbakery.ThirdPartyCaveatCheckerParams) ([]checkers.Caveat, error) { 161 | cond, arg, err := checkers.ParseCaveat(string(p.Caveat.Condition)) 162 | if err != nil { 163 | return nil, err 164 | } 165 | return check(cond, arg) 166 | } 167 | return httpbakery.ThirdPartyCaveatCheckerPFunc(f) 168 | } 169 | 170 | // ConditionParserP adapts the given function into an httpbakery.ThirdPartyCaveatChecker. 171 | // It parses the caveat's condition and calls the function with the result. 172 | func ConditionParserP(check func(cond, arg string) ([]checkers.Caveat, error)) httpbakery.ThirdPartyCaveatChecker { 173 | f := func(ctx context.Context, req *http.Request, cav *bakery.ThirdPartyCaveatInfo, token *httpbakery.DischargeToken) ([]checkers.Caveat, error) { 174 | cond, arg, err := checkers.ParseCaveat(string(cav.Condition)) 175 | if err != nil { 176 | return nil, err 177 | } 178 | return check(cond, arg) 179 | } 180 | return httpbakery.ThirdPartyCaveatCheckerFunc(f) 181 | } 182 | 183 | var skipVerify struct { 184 | mu sync.Mutex 185 | refCount int 186 | oldSkipVerify bool 187 | } 188 | 189 | func startSkipVerify() { 190 | v := &skipVerify 191 | v.mu.Lock() 192 | defer v.mu.Unlock() 193 | if v.refCount++; v.refCount > 1 { 194 | return 195 | } 196 | transport, ok := http.DefaultTransport.(*http.Transport) 197 | if !ok { 198 | return 199 | } 200 | if transport.TLSClientConfig != nil { 201 | v.oldSkipVerify = transport.TLSClientConfig.InsecureSkipVerify 202 | transport.TLSClientConfig.InsecureSkipVerify = true 203 | } else { 204 | v.oldSkipVerify = false 205 | transport.TLSClientConfig = &tls.Config{ 206 | InsecureSkipVerify: true, 207 | } 208 | } 209 | } 210 | 211 | func stopSkipVerify() { 212 | v := &skipVerify 213 | v.mu.Lock() 214 | defer v.mu.Unlock() 215 | if v.refCount--; v.refCount > 0 { 216 | return 217 | } 218 | transport, ok := http.DefaultTransport.(*http.Transport) 219 | if !ok { 220 | return 221 | } 222 | // technically this doesn't return us to the original state, 223 | // as TLSClientConfig may have been nil before but won't 224 | // be now, but that should be equivalent. 225 | transport.TLSClientConfig.InsecureSkipVerify = v.oldSkipVerify 226 | } 227 | -------------------------------------------------------------------------------- /bakerytest/rendezvous.go: -------------------------------------------------------------------------------- 1 | package bakerytest 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "sync" 7 | "time" 8 | 9 | "gopkg.in/errgo.v1" 10 | 11 | "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" 12 | "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/checkers" 13 | "github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery" 14 | ) 15 | 16 | // Rendezvous implements a place where discharge information 17 | // can be stored, recovered and waited for. 18 | type Rendezvous struct { 19 | mu sync.Mutex 20 | maxId int 21 | waiting map[string]*dischargeFuture 22 | } 23 | 24 | func NewRendezvous() *Rendezvous { 25 | return &Rendezvous{ 26 | waiting: make(map[string]*dischargeFuture), 27 | } 28 | } 29 | 30 | type dischargeFuture struct { 31 | info *bakery.ThirdPartyCaveatInfo 32 | done chan struct{} 33 | caveats []checkers.Caveat 34 | err error 35 | } 36 | 37 | // NewDischarge creates a new discharge in the rendezvous 38 | // associated with the given caveat information. 39 | // It returns an identifier for the discharge that can 40 | // later be used to complete the discharge or find 41 | // out the information again. 42 | func (r *Rendezvous) NewDischarge(cav *bakery.ThirdPartyCaveatInfo) string { 43 | r.mu.Lock() 44 | defer r.mu.Unlock() 45 | dischargeId := fmt.Sprintf("%d", r.maxId) 46 | r.maxId++ 47 | r.waiting[dischargeId] = &dischargeFuture{ 48 | info: cav, 49 | done: make(chan struct{}), 50 | } 51 | return dischargeId 52 | } 53 | 54 | // Info returns information on the given discharge id 55 | // and reports whether the information has been found. 56 | func (r *Rendezvous) Info(dischargeId string) (*bakery.ThirdPartyCaveatInfo, bool) { 57 | r.mu.Lock() 58 | defer r.mu.Unlock() 59 | d := r.waiting[dischargeId] 60 | if d == nil { 61 | return nil, false 62 | } 63 | return d.info, true 64 | } 65 | 66 | // DischargeComplete marks the discharge with the given id 67 | // as completed with the given caveats, 68 | // which will be associated with the given discharge id 69 | // and returned from Await. 70 | func (r *Rendezvous) DischargeComplete(dischargeId string, caveats []checkers.Caveat) { 71 | r.dischargeDone(dischargeId, caveats, nil) 72 | } 73 | 74 | // DischargeFailed marks the discharge with the given id 75 | // as failed with the given error, which will be 76 | // returned from Await or CheckToken when they're 77 | // called with that id. 78 | func (r *Rendezvous) DischargeFailed(dischargeId string, err error) { 79 | r.dischargeDone(dischargeId, nil, err) 80 | } 81 | 82 | func (r *Rendezvous) dischargeDone(dischargeId string, caveats []checkers.Caveat, err error) { 83 | r.mu.Lock() 84 | defer r.mu.Unlock() 85 | d := r.waiting[dischargeId] 86 | if d == nil { 87 | panic(errgo.Newf("invalid discharge id %q", dischargeId)) 88 | } 89 | select { 90 | case <-d.done: 91 | panic(errgo.Newf("DischargeComplete called twice")) 92 | default: 93 | } 94 | d.caveats, d.err = caveats, err 95 | close(d.done) 96 | } 97 | 98 | // Await waits for DischargeComplete or DischargeFailed to be called, 99 | // and returns either the caveats passed to DischargeComplete 100 | // or the error passed to DischargeFailed. 101 | // 102 | // It waits for at least the given duration. If timeout is zero, 103 | // it returns the information only if it is already available. 104 | func (r *Rendezvous) Await(dischargeId string, timeout time.Duration) ([]checkers.Caveat, error) { 105 | r.mu.Lock() 106 | d := r.waiting[dischargeId] 107 | r.mu.Unlock() 108 | if d == nil { 109 | return nil, errgo.Newf("invalid discharge id %q", dischargeId) 110 | } 111 | if timeout == 0 { 112 | select { 113 | case <-d.done: 114 | default: 115 | return nil, errgo.New("rendezvous has not completed") 116 | } 117 | } else { 118 | select { 119 | case <-d.done: 120 | case <-time.After(timeout): 121 | return nil, errgo.New("timeout waiting for rendezvous to complete") 122 | } 123 | } 124 | if d.err != nil { 125 | return nil, errgo.Mask(d.err, errgo.Any) 126 | } 127 | return d.caveats, nil 128 | } 129 | 130 | func (r *Rendezvous) DischargeToken(dischargeId string) *httpbakery.DischargeToken { 131 | _, err := r.Await(dischargeId, 0) 132 | if err != nil { 133 | panic(errgo.Notef(err, "cannot obtain discharge token for %q", dischargeId)) 134 | } 135 | return &httpbakery.DischargeToken{ 136 | Kind: "discharge-id", 137 | Value: []byte(dischargeId), 138 | } 139 | } 140 | 141 | // CheckToken checks that the given token is valid for discharging the 142 | // given caveat, and returns any caveats passed to DischargeComplete 143 | // if it is. 144 | func (r *Rendezvous) CheckToken(token *httpbakery.DischargeToken, cav *bakery.ThirdPartyCaveatInfo) ([]checkers.Caveat, error) { 145 | if token.Kind != "discharge-id" { 146 | return nil, errgo.Newf("invalid discharge token kind %q", token.Kind) 147 | } 148 | info, ok := r.Info(string(token.Value)) 149 | if !ok { 150 | return nil, errgo.Newf("discharge token %q not found", token.Value) 151 | } 152 | if !bytes.Equal(info.Caveat, cav.Caveat) { 153 | return nil, errgo.Newf("caveat provided to CheckToken does not match original") 154 | } 155 | if !bytes.Equal(info.Id, cav.Id) { 156 | return nil, errgo.Newf("caveat id provided to CheckToken does not match original") 157 | } 158 | caveats, err := r.Await(string(token.Value), 0) 159 | if err != nil { 160 | // Don't mask the error because we want the cause to remain 161 | // unchanged if it was passed to DischargeFailed. 162 | return nil, errgo.Mask(err, errgo.Any) 163 | } 164 | return caveats, nil 165 | } 166 | -------------------------------------------------------------------------------- /cmd/bakery-keygen/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/go-macaroon-bakery/macaroon-bakery/cmd/bakery-keygen/v3 2 | 3 | go 1.21 4 | 5 | require github.com/go-macaroon-bakery/macaroon-bakery/v3 v3.0.1 6 | 7 | require ( 8 | github.com/go-macaroon-bakery/macaroonpb v1.0.0 // indirect 9 | github.com/golang/protobuf v1.4.3 // indirect 10 | github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af // indirect 11 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 // indirect 12 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 // indirect 13 | google.golang.org/protobuf v1.25.0 // indirect 14 | gopkg.in/errgo.v1 v1.0.1 // indirect 15 | gopkg.in/macaroon.v2 v2.1.0 // indirect 16 | ) 17 | -------------------------------------------------------------------------------- /cmd/bakery-keygen/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "os" 7 | 8 | "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" 9 | ) 10 | 11 | func main() { 12 | kp, err := bakery.GenerateKey() 13 | if err != nil { 14 | fmt.Fprintf(os.Stderr, "cannot generate key: %s\n", err) 15 | os.Exit(1) 16 | } 17 | b, err := json.MarshalIndent(kp, "", "\t") 18 | if err != nil { 19 | panic(err) 20 | } 21 | fmt.Printf("%s\n", b) 22 | } 23 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/go-macaroon-bakery/macaroon-bakery/v3 2 | 3 | go 1.17 4 | 5 | require ( 6 | github.com/frankban/quicktest v1.11.3 7 | github.com/go-macaroon-bakery/macaroonpb v1.0.0 8 | github.com/google/go-cmp v0.5.4 9 | github.com/juju/mgo/v2 v2.0.0-20220111072304-f200228f1090 10 | github.com/juju/mgotest v1.0.3 11 | github.com/juju/postgrestest v1.1.0 12 | github.com/juju/qthttptest v0.1.3 13 | github.com/juju/webbrowser v0.0.0-20160309143629-54b8c57083b4 14 | github.com/julienschmidt/httprouter v1.3.0 15 | github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af 16 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 17 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 18 | gopkg.in/errgo.v1 v1.0.1 19 | gopkg.in/httprequest.v1 v1.2.1 20 | gopkg.in/juju/environschema.v1 v1.0.0 21 | gopkg.in/macaroon.v2 v2.1.0 22 | gopkg.in/yaml.v2 v2.4.0 23 | ) 24 | 25 | require ( 26 | github.com/golang/protobuf v1.4.3 // indirect 27 | github.com/juju/schema v1.0.0 // indirect 28 | github.com/kr/pretty v0.2.1 // indirect 29 | github.com/kr/text v0.2.0 // indirect 30 | github.com/lib/pq v1.3.0 // indirect 31 | github.com/xdg-go/stringprep v1.0.2 // indirect 32 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 // indirect 33 | golang.org/x/text v0.3.5 // indirect 34 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 // indirect 35 | google.golang.org/protobuf v1.25.0 // indirect 36 | gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22 // indirect 37 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect 38 | ) 39 | -------------------------------------------------------------------------------- /httpbakery/agent/agent_test.go: -------------------------------------------------------------------------------- 1 | package agent_test 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "io/ioutil" 7 | "net/http" 8 | "os" 9 | "testing" 10 | 11 | qt "github.com/frankban/quicktest" 12 | "gopkg.in/errgo.v1" 13 | "gopkg.in/httprequest.v1" 14 | "gopkg.in/macaroon.v2" 15 | 16 | "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" 17 | "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/checkers" 18 | "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakerytest" 19 | "github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery" 20 | "github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery/agent" 21 | ) 22 | 23 | var agentLoginOp = bakery.Op{"agent", "login"} 24 | 25 | func TestSetUpAuth(t *testing.T) { 26 | c := qt.New(t) 27 | defer c.Done() 28 | f := newAgentFixture(c) 29 | dischargerBakery := bakery.New(bakery.BakeryParams{ 30 | Key: f.discharger.Key, 31 | }) 32 | f.discharger.AddHTTPHandlers(AgentHandlers(AgentHandler{ 33 | AgentMacaroon: func(p httprequest.Params, username string, pubKey *bakery.PublicKey) (*bakery.Macaroon, error) { 34 | if username != "test-user" || *pubKey != f.agentBakery.Oven.Key().Public { 35 | return nil, errgo.Newf("mismatched user/pubkey; want %s got %s", f.agentBakery.Oven.Key().Public, *pubKey) 36 | } 37 | version := httpbakery.RequestVersion(p.Request) 38 | return dischargerBakery.Oven.NewMacaroon( 39 | context.Background(), 40 | bakery.LatestVersion, 41 | []checkers.Caveat{ 42 | bakery.LocalThirdPartyCaveat(pubKey, version), 43 | }, 44 | agentLoginOp, 45 | ) 46 | }, 47 | })) 48 | f.discharger.Checker = httpbakery.ThirdPartyCaveatCheckerFunc(func(ctx context.Context, req *http.Request, info *bakery.ThirdPartyCaveatInfo, token *httpbakery.DischargeToken) ([]checkers.Caveat, error) { 49 | if token != nil { 50 | c.Logf("with token request: %v", req.URL) 51 | if token.Kind != "agent" { 52 | return nil, errgo.Newf("unexpected discharge token kind %q", token.Kind) 53 | } 54 | var m macaroon.Slice 55 | if err := m.UnmarshalBinary(token.Value); err != nil { 56 | return nil, errgo.Notef(err, "cannot unmarshal token") 57 | } 58 | if _, err := dischargerBakery.Checker.Auth(m).Allow(ctx, agentLoginOp); err != nil { 59 | return nil, errgo.Newf("received unexpected discharge token") 60 | } 61 | return nil, nil 62 | } 63 | if string(info.Condition) != "some-third-party-caveat" { 64 | return nil, errgo.Newf("unexpected caveat condition") 65 | } 66 | err := httpbakery.NewInteractionRequiredError(nil, req) 67 | agent.SetInteraction(err, "/agent-macaroon") 68 | return nil, err 69 | }) 70 | 71 | client := httpbakery.NewClient() 72 | err := agent.SetUpAuth(client, &agent.AuthInfo{ 73 | Key: f.agentBakery.Oven.Key(), 74 | Agents: []agent.Agent{{ 75 | URL: f.discharger.Location(), 76 | Username: "test-user", 77 | }}, 78 | }) 79 | someOp := bakery.Op{ 80 | Entity: "something", 81 | Action: "doit", 82 | } 83 | c.Assert(err, qt.IsNil) 84 | m, err := f.serverBakery.Oven.NewMacaroon( 85 | context.Background(), 86 | bakery.LatestVersion, 87 | []checkers.Caveat{{ 88 | Location: f.discharger.Location(), 89 | Condition: "some-third-party-caveat", 90 | }}, 91 | someOp, 92 | ) 93 | c.Assert(err, qt.Equals, nil) 94 | ms, err := client.DischargeAll(context.Background(), m) 95 | c.Assert(err, qt.Equals, nil) 96 | _, err = f.serverBakery.Checker.Auth(ms).Allow(context.Background(), someOp) 97 | c.Assert(err, qt.Equals, nil) 98 | } 99 | 100 | func TestAuthInfoFromEnvironment(t *testing.T) { 101 | c := qt.New(t) 102 | defer c.Done() 103 | defer os.Setenv("BAKERY_AGENT_FILE", "") 104 | 105 | f, err := ioutil.TempFile("", "") 106 | c.Assert(err, qt.Equals, nil) 107 | defer os.Remove(f.Name()) 108 | defer f.Close() 109 | 110 | authInfo := &agent.AuthInfo{ 111 | Key: bakery.MustGenerateKey(), 112 | Agents: []agent.Agent{{ 113 | URL: "https://0.1.2.3/x", 114 | Username: "bob", 115 | }, { 116 | URL: "https://0.2.3.4", 117 | Username: "charlie", 118 | }}, 119 | } 120 | data, err := json.Marshal(authInfo) 121 | _, err = f.Write(data) 122 | c.Assert(err, qt.Equals, nil) 123 | f.Close() 124 | 125 | os.Setenv("BAKERY_AGENT_FILE", f.Name()) 126 | 127 | authInfo1, err := agent.AuthInfoFromEnvironment() 128 | c.Assert(err, qt.Equals, nil) 129 | c.Assert(authInfo1, qt.DeepEquals, authInfo) 130 | } 131 | 132 | func TestAuthInfoFromEnvironmentNotSet(t *testing.T) { 133 | c := qt.New(t) 134 | defer c.Done() 135 | os.Setenv("BAKERY_AGENT_FILE", "") 136 | authInfo, err := agent.AuthInfoFromEnvironment() 137 | c.Assert(errgo.Cause(err), qt.Equals, agent.ErrNoAuthInfo) 138 | c.Assert(authInfo, qt.IsNil) 139 | } 140 | 141 | type agentFixture struct { 142 | agentBakery *bakery.Bakery 143 | serverBakery *bakery.Bakery 144 | discharger *bakerytest.Discharger 145 | } 146 | 147 | func newAgentFixture(c *qt.C) *agentFixture { 148 | var f agentFixture 149 | f.discharger = bakerytest.NewDischarger(nil) 150 | c.Defer(f.discharger.Close) 151 | f.agentBakery = bakery.New(bakery.BakeryParams{ 152 | Key: bakery.MustGenerateKey(), 153 | }) 154 | f.serverBakery = bakery.New(bakery.BakeryParams{ 155 | Locator: f.discharger, 156 | Key: bakery.MustGenerateKey(), 157 | }) 158 | return &f 159 | } 160 | 161 | func AgentHandlers(h AgentHandler) []httprequest.Handler { 162 | return reqServer.Handlers(func(p httprequest.Params) (agentHandlers, context.Context, error) { 163 | return agentHandlers{h}, p.Context, nil 164 | }) 165 | } 166 | 167 | // AgentHandler holds the functions that may be called by the 168 | // agent-interaction server. 169 | type AgentHandler struct { 170 | AgentMacaroon func(p httprequest.Params, username string, pubKey *bakery.PublicKey) (*bakery.Macaroon, error) 171 | } 172 | 173 | // agentHandlers is used to define the handler methods. 174 | type agentHandlers struct { 175 | h AgentHandler 176 | } 177 | 178 | // agentMacaroonRequest represents a request for the 179 | // agent macaroon - it matches agent.agentMacaroonRequest. 180 | type agentMacaroonRequest struct { 181 | httprequest.Route `httprequest:"GET /agent-macaroon"` 182 | Username string `httprequest:"username,form"` 183 | PublicKey *bakery.PublicKey `httprequest:"public-key,form"` 184 | } 185 | 186 | type agentMacaroonResponse struct { 187 | Macaroon *bakery.Macaroon `json:"macaroon"` 188 | } 189 | 190 | func (h agentHandlers) AgentMacaroon(p httprequest.Params, r *agentMacaroonRequest) (*agentMacaroonResponse, error) { 191 | m, err := h.h.AgentMacaroon(p, r.Username, r.PublicKey) 192 | if err != nil { 193 | return nil, errgo.Mask(err, errgo.Any) 194 | } 195 | return &agentMacaroonResponse{ 196 | Macaroon: m, 197 | }, nil 198 | } 199 | -------------------------------------------------------------------------------- /httpbakery/agent/cookie.go: -------------------------------------------------------------------------------- 1 | package agent 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | 7 | "gopkg.in/errgo.v1" 8 | "gopkg.in/macaroon.v2" 9 | 10 | "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" 11 | ) 12 | 13 | const cookieName = "agent-login" 14 | 15 | // agentLogin defines the structure of an agent login cookie. 16 | type agentLogin struct { 17 | Username string `json:"username"` 18 | PublicKey *bakery.PublicKey `json:"public_key"` 19 | } 20 | 21 | // ErrNoAgentLoginCookie is the error returned when the expected 22 | // agent login cookie has not been found. 23 | var ErrNoAgentLoginCookie = errgo.New("no agent-login cookie found") 24 | 25 | // LoginCookie returns details of the agent login cookie 26 | // from the given request. If no agent-login cookie is found, 27 | // it returns an ErrNoAgentLoginCookie error. 28 | // 29 | // This function is only applicable to the legacy agent 30 | // protocol and will be deprecated in the future. 31 | func LoginCookie(req *http.Request) (username string, key *bakery.PublicKey, err error) { 32 | c, err := req.Cookie(cookieName) 33 | if err != nil { 34 | return "", nil, ErrNoAgentLoginCookie 35 | } 36 | b, err := macaroon.Base64Decode([]byte(c.Value)) 37 | if err != nil { 38 | return "", nil, errgo.Notef(err, "cannot decode cookie value") 39 | } 40 | var al agentLogin 41 | if err := json.Unmarshal(b, &al); err != nil { 42 | return "", nil, errgo.Notef(err, "cannot unmarshal agent login") 43 | } 44 | if al.Username == "" { 45 | return "", nil, errgo.Newf("agent login has no user name") 46 | } 47 | if al.PublicKey == nil { 48 | return "", nil, errgo.Newf("agent login has no public key") 49 | } 50 | return al.Username, al.PublicKey, nil 51 | } 52 | -------------------------------------------------------------------------------- /httpbakery/agent/cookie_test.go: -------------------------------------------------------------------------------- 1 | package agent_test 2 | 3 | import ( 4 | "encoding/base64" 5 | "encoding/json" 6 | "net/http" 7 | "testing" 8 | 9 | qt "github.com/frankban/quicktest" 10 | "gopkg.in/errgo.v1" 11 | 12 | "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" 13 | "github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery/agent" 14 | ) 15 | 16 | var loginCookieTests = []struct { 17 | about string 18 | addCookie func(*http.Request, *bakery.PublicKey) 19 | expectUser string 20 | expectError string 21 | expectCause error 22 | }{{ 23 | about: "success", 24 | addCookie: func(req *http.Request, key *bakery.PublicKey) { 25 | addCookie(req, "bob", key) 26 | }, 27 | expectUser: "bob", 28 | }, { 29 | about: "no cookie", 30 | addCookie: func(req *http.Request, key *bakery.PublicKey) {}, 31 | expectError: "no agent-login cookie found", 32 | expectCause: agent.ErrNoAgentLoginCookie, 33 | }, { 34 | about: "invalid base64 encoding", 35 | addCookie: func(req *http.Request, key *bakery.PublicKey) { 36 | req.AddCookie(&http.Cookie{ 37 | Name: "agent-login", 38 | Value: "x", 39 | }) 40 | }, 41 | expectError: "cannot decode cookie value: illegal base64 data at input byte 0", 42 | }, { 43 | about: "invalid JSON", 44 | addCookie: func(req *http.Request, key *bakery.PublicKey) { 45 | req.AddCookie(&http.Cookie{ 46 | Name: "agent-login", 47 | Value: base64.StdEncoding.EncodeToString([]byte("}")), 48 | }) 49 | }, 50 | expectError: "cannot unmarshal agent login: invalid character '}' looking for beginning of value", 51 | }, { 52 | about: "no username", 53 | addCookie: func(req *http.Request, key *bakery.PublicKey) { 54 | addCookie(req, "", key) 55 | }, 56 | expectError: "agent login has no user name", 57 | }, { 58 | about: "no public key", 59 | addCookie: func(req *http.Request, key *bakery.PublicKey) { 60 | addCookie(req, "bob", nil) 61 | }, 62 | expectError: "agent login has no public key", 63 | }} 64 | 65 | func TestLoginCookie(t *testing.T) { 66 | c := qt.New(t) 67 | key, err := bakery.GenerateKey() 68 | c.Assert(err, qt.IsNil) 69 | 70 | for i, test := range loginCookieTests { 71 | c.Logf("test %d: %s", i, test.about) 72 | 73 | req, err := http.NewRequest("GET", "", nil) 74 | c.Assert(err, qt.IsNil) 75 | test.addCookie(req, &key.Public) 76 | gotUsername, gotKey, err := agent.LoginCookie(req) 77 | 78 | if test.expectError != "" { 79 | c.Assert(err, qt.ErrorMatches, test.expectError) 80 | if test.expectCause != nil { 81 | c.Assert(errgo.Cause(err), qt.Equals, test.expectCause) 82 | } 83 | continue 84 | } 85 | c.Assert(gotUsername, qt.Equals, test.expectUser) 86 | c.Assert(gotKey, qt.DeepEquals, &key.Public) 87 | } 88 | } 89 | 90 | // addCookie adds an agent-login cookie with the specified parameters to 91 | // the given request. 92 | func addCookie(req *http.Request, username string, key *bakery.PublicKey) { 93 | al := agent.AgentLogin{ 94 | Username: username, 95 | PublicKey: key, 96 | } 97 | data, err := json.Marshal(al) 98 | if err != nil { 99 | // This should be impossible as the agentLogin structure 100 | // has to be marshalable. It is certainly a bug if it 101 | // isn't. 102 | panic(errgo.Notef(err, "cannot marshal %s cookie", agent.CookieName)) 103 | } 104 | req.AddCookie(&http.Cookie{ 105 | Name: agent.CookieName, 106 | Value: base64.StdEncoding.EncodeToString(data), 107 | }) 108 | } 109 | -------------------------------------------------------------------------------- /httpbakery/agent/example_test.go: -------------------------------------------------------------------------------- 1 | package agent_test 2 | 3 | import ( 4 | "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" 5 | "github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery" 6 | "github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery/agent" 7 | ) 8 | 9 | func ExampleSetUpAuth() { 10 | // In practice the key would be read from persistent 11 | // storage. 12 | key, err := bakery.GenerateKey() 13 | if err != nil { 14 | // handle error 15 | } 16 | 17 | client := httpbakery.NewClient() 18 | err = agent.SetUpAuth(client, &agent.AuthInfo{ 19 | Key: key, 20 | Agents: []agent.Agent{{ 21 | URL: "http://foo.com", 22 | Username: "agent-username", 23 | }}, 24 | }) 25 | if err != nil { 26 | // handle error 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /httpbakery/agent/export_test.go: -------------------------------------------------------------------------------- 1 | package agent 2 | 3 | type AgentLogin agentLogin 4 | 5 | const CookieName = cookieName 6 | -------------------------------------------------------------------------------- /httpbakery/agent/protocol.go: -------------------------------------------------------------------------------- 1 | package agent 2 | 3 | /* 4 | PROTOCOL 5 | 6 | The agent protocol is initiated when attempting to perform a 7 | discharge. It works as follows: 8 | 9 | A is Agent 10 | L is Login Service 11 | 12 | A->L 13 | POST /discharge 14 | L->A 15 | Interaction-required error containing 16 | an entry for "agent" with field 17 | "login-url" holding URL $loginURL. 18 | A->L 19 | GET $loginURL?username=$user&public-key=$pubkey 20 | where $user is the username to log in as 21 | and $pubkey is the public key of that username, 22 | base-64 encoded. 23 | L->A 24 | JSON response: 25 | macaroon: 26 | macaroon with "local" third-party-caveat 27 | addressed to $pubkey. 28 | A->L 29 | POST /discharge?token-kind=agent&token64=self-discharged macaroon 30 | The macaroon is binary-encoded, then base64 31 | encoded. Note that, as with most Go HTTP handlers, the parameters 32 | may also be in the form-encoded request body. 33 | L->A 34 | discharge macaroon 35 | 36 | A local third-party caveat is a third party caveat with the location 37 | set to "local" and the caveat encrypted with the public key specified in the GET request. 38 | 39 | LEGACY PROTOCOL 40 | 41 | The legacy agent protocol is used by services that don't yet 42 | implement the new protocol. Once a discharge has 43 | failed with an interaction required error, an agent login works 44 | as follows: 45 | 46 | Agent Login Service 47 | | | 48 | | GET visitURL with agent cookie | 49 | |----------------------------------->| 50 | | | 51 | | Macaroon with local third-party | 52 | | caveat | 53 | |<-----------------------------------| 54 | | | 55 | | GET visitURL with agent cookie & | 56 | | discharged macaroon | 57 | |----------------------------------->| 58 | | | 59 | | Agent login response | 60 | |<-----------------------------------| 61 | | | 62 | 63 | The agent cookie is a cookie in the same form described in the 64 | PROTOCOL section above. 65 | 66 | On success the response is the following JSON object: 67 | 68 | { 69 | "agent_login": "true" 70 | } 71 | 72 | If an error occurs then the response should be a JSON object that 73 | unmarshals to an httpbakery.Error. 74 | */ 75 | -------------------------------------------------------------------------------- /httpbakery/browser.go: -------------------------------------------------------------------------------- 1 | package httpbakery 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | "net/url" 8 | "os" 9 | 10 | "github.com/juju/webbrowser" 11 | "gopkg.in/errgo.v1" 12 | "gopkg.in/httprequest.v1" 13 | 14 | "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" 15 | ) 16 | 17 | const WebBrowserInteractionKind = "browser-window" 18 | 19 | // WaitTokenResponse holds the response type 20 | // returned, JSON-encoded, from the waitToken 21 | // URL passed to SetBrowserInteraction. 22 | type WaitTokenResponse struct { 23 | Kind string `json:"kind"` 24 | // Token holds the token value when it's well-formed utf-8 25 | Token string `json:"token,omitempty"` 26 | // Token64 holds the token value, base64 encoded, when it's 27 | // not well-formed utf-8. 28 | Token64 string `json:"token64,omitempty"` 29 | } 30 | 31 | // WaitResponse holds the type that should be returned 32 | // by an HTTP response made to a LegacyWaitURL 33 | // (See the ErrorInfo type). 34 | type WaitResponse struct { 35 | Macaroon *bakery.Macaroon 36 | } 37 | 38 | // WebBrowserInteractionInfo holds the information 39 | // expected in the browser-window interaction 40 | // entry in an interaction-required error. 41 | type WebBrowserInteractionInfo struct { 42 | // VisitURL holds the URL to be visited in a web browser. 43 | VisitURL string 44 | 45 | // WaitTokenURL holds a URL that will block on GET 46 | // until the browser interaction has completed. 47 | // On success, the response is expected to hold a waitTokenResponse 48 | // in its body holding the token to be returned from the 49 | // Interact method. 50 | WaitTokenURL string 51 | } 52 | 53 | var ( 54 | _ Interactor = WebBrowserInteractor{} 55 | _ LegacyInteractor = WebBrowserInteractor{} 56 | ) 57 | 58 | // OpenWebBrowser opens a web browser at the 59 | // given URL. If the OS is not recognised, the URL 60 | // is just printed to standard output. 61 | func OpenWebBrowser(url *url.URL) error { 62 | err := webbrowser.Open(url) 63 | if err == nil { 64 | fmt.Fprintf(os.Stderr, "Opening an authorization web page in your browser.\n") 65 | fmt.Fprintf(os.Stderr, "If it does not open, please open this URL:\n%s\n", url) 66 | return nil 67 | } 68 | if err == webbrowser.ErrNoBrowser { 69 | fmt.Fprintf(os.Stderr, "Please open this URL in your browser to authorize:\n%s\n", url) 70 | return nil 71 | } 72 | return err 73 | } 74 | 75 | // SetWebBrowserInteraction adds information about web-browser-based 76 | // interaction to the given error, which should be an 77 | // interaction-required error that's about to be returned from a 78 | // discharge request. 79 | // 80 | // The visitURL parameter holds a URL that should be visited by the user 81 | // in a web browser; the waitTokenURL parameter holds a URL that can be 82 | // long-polled to acquire the resulting discharge token. 83 | // 84 | // Use SetLegacyInteraction to add support for legacy clients 85 | // that don't understand the newer InteractionMethods field. 86 | func SetWebBrowserInteraction(e *Error, visitURL, waitTokenURL string) { 87 | e.SetInteraction(WebBrowserInteractionKind, WebBrowserInteractionInfo{ 88 | VisitURL: visitURL, 89 | WaitTokenURL: waitTokenURL, 90 | }) 91 | } 92 | 93 | // SetLegacyInteraction adds information about web-browser-based 94 | // interaction (or other kinds of legacy-protocol interaction) to the 95 | // given error, which should be an interaction-required error that's 96 | // about to be returned from a discharge request. 97 | // 98 | // The visitURL parameter holds a URL that should be visited by the user 99 | // in a web browser (or with an "Accept: application/json" header to 100 | // find out the set of legacy interaction methods). 101 | // 102 | // The waitURL parameter holds a URL that can be long-polled 103 | // to acquire the discharge macaroon. 104 | func SetLegacyInteraction(e *Error, visitURL, waitURL string) { 105 | if e.Info == nil { 106 | e.Info = new(ErrorInfo) 107 | } 108 | e.Info.LegacyVisitURL = visitURL 109 | e.Info.LegacyWaitURL = waitURL 110 | } 111 | 112 | // WebBrowserInteractor handls web-browser-based 113 | // interaction-required errors by opening a web 114 | // browser to allow the user to prove their 115 | // credentials interactively. 116 | // 117 | // It implements the Interactor interface, so instances 118 | // can be used with Client.AddInteractor. 119 | type WebBrowserInteractor struct { 120 | // OpenWebBrowser is used to visit a page in 121 | // the user's web browser. If it's nil, the 122 | // OpenWebBrowser function will be used. 123 | OpenWebBrowser func(*url.URL) error 124 | } 125 | 126 | // Kind implements Interactor.Kind. 127 | func (WebBrowserInteractor) Kind() string { 128 | return WebBrowserInteractionKind 129 | } 130 | 131 | // Interact implements Interactor.Interact by opening a new web page. 132 | func (wi WebBrowserInteractor) Interact(ctx context.Context, client *Client, location string, irErr *Error) (*DischargeToken, error) { 133 | var p WebBrowserInteractionInfo 134 | if err := irErr.InteractionMethod(wi.Kind(), &p); err != nil { 135 | return nil, errgo.Mask(err, errgo.Is(ErrInteractionMethodNotFound)) 136 | } 137 | visitURL, err := relativeURL(location, p.VisitURL) 138 | if err != nil { 139 | return nil, errgo.Notef(err, "cannot make relative visit URL") 140 | } 141 | waitTokenURL, err := relativeURL(location, p.WaitTokenURL) 142 | if err != nil { 143 | return nil, errgo.Notef(err, "cannot make relative wait URL") 144 | } 145 | if err := wi.openWebBrowser(visitURL); err != nil { 146 | return nil, errgo.Mask(err) 147 | } 148 | return waitForToken(ctx, client, waitTokenURL) 149 | } 150 | 151 | func (wi WebBrowserInteractor) openWebBrowser(u *url.URL) error { 152 | open := wi.OpenWebBrowser 153 | if open == nil { 154 | open = OpenWebBrowser 155 | } 156 | if err := open(u); err != nil { 157 | return errgo.Mask(err) 158 | } 159 | return nil 160 | } 161 | 162 | // waitForToken returns a token from a the waitToken URL 163 | func waitForToken(ctx context.Context, client *Client, waitTokenURL *url.URL) (*DischargeToken, error) { 164 | // TODO integrate this with waitForMacaroon somehow? 165 | req, err := http.NewRequest("GET", waitTokenURL.String(), nil) 166 | if err != nil { 167 | return nil, errgo.Mask(err) 168 | } 169 | req = req.WithContext(ctx) 170 | httpResp, err := client.Client.Do(req) 171 | if err != nil { 172 | return nil, errgo.Notef(err, "cannot get %q", waitTokenURL) 173 | } 174 | defer httpResp.Body.Close() 175 | if httpResp.StatusCode != http.StatusOK { 176 | err := unmarshalError(httpResp) 177 | return nil, errgo.NoteMask(err, "cannot acquire discharge token", errgo.Any) 178 | } 179 | var resp WaitTokenResponse 180 | if err := httprequest.UnmarshalJSONResponse(httpResp, &resp); err != nil { 181 | return nil, errgo.Notef(err, "cannot unmarshal wait response") 182 | } 183 | tokenVal, err := maybeBase64Decode(resp.Token, resp.Token64) 184 | if err != nil { 185 | return nil, errgo.Notef(err, "bad discharge token") 186 | } 187 | // TODO check that kind and value are non-empty? 188 | return &DischargeToken{ 189 | Kind: resp.Kind, 190 | Value: tokenVal, 191 | }, nil 192 | } 193 | 194 | // LegacyInteract implements LegacyInteractor by opening a web browser page. 195 | func (wi WebBrowserInteractor) LegacyInteract(ctx context.Context, client *Client, location string, visitURL *url.URL) error { 196 | if err := wi.openWebBrowser(visitURL); err != nil { 197 | return errgo.Mask(err) 198 | } 199 | return nil 200 | } 201 | -------------------------------------------------------------------------------- /httpbakery/checkers.go: -------------------------------------------------------------------------------- 1 | package httpbakery 2 | 3 | import ( 4 | "context" 5 | "net" 6 | "net/http" 7 | 8 | "gopkg.in/errgo.v1" 9 | 10 | "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/checkers" 11 | ) 12 | 13 | type httpRequestKey struct{} 14 | 15 | // ContextWithRequest returns the context with information from the 16 | // given request attached as context. This is used by the httpbakery 17 | // checkers (see RegisterCheckers for details). 18 | func ContextWithRequest(ctx context.Context, req *http.Request) context.Context { 19 | return context.WithValue(ctx, httpRequestKey{}, req) 20 | } 21 | 22 | func requestFromContext(ctx context.Context) *http.Request { 23 | req, _ := ctx.Value(httpRequestKey{}).(*http.Request) 24 | return req 25 | } 26 | 27 | const ( 28 | // CondClientIPAddr holds the first party caveat condition 29 | // that checks a client's IP address. 30 | CondClientIPAddr = "client-ip-addr" 31 | 32 | // CondClientOrigin holds the first party caveat condition that 33 | // checks a client's origin header. 34 | CondClientOrigin = "origin" 35 | ) 36 | 37 | // CheckersNamespace holds the URI of the HTTP checkers schema. 38 | const CheckersNamespace = "http" 39 | 40 | var allCheckers = map[string]checkers.Func{ 41 | CondClientIPAddr: ipAddrCheck, 42 | CondClientOrigin: clientOriginCheck, 43 | } 44 | 45 | // RegisterCheckers registers all the HTTP checkers with the given checker. 46 | // Current checkers include: 47 | // 48 | // client-ip-addr 49 | // 50 | // The client-ip-addr caveat checks that the HTTP request has 51 | // the given remote IP address. 52 | // 53 | // origin 54 | // 55 | // The origin caveat checks that the HTTP Origin header has 56 | // the given value. 57 | func RegisterCheckers(c *checkers.Checker) { 58 | c.Namespace().Register(CheckersNamespace, "http") 59 | for cond, check := range allCheckers { 60 | c.Register(cond, CheckersNamespace, check) 61 | } 62 | } 63 | 64 | // NewChecker returns a new checker with the standard 65 | // and HTTP checkers registered in it. 66 | func NewChecker() *checkers.Checker { 67 | c := checkers.New(nil) 68 | RegisterCheckers(c) 69 | return c 70 | } 71 | 72 | // ipAddrCheck implements the IP client address checker 73 | // for an HTTP request. 74 | func ipAddrCheck(ctx context.Context, cond, args string) error { 75 | req := requestFromContext(ctx) 76 | if req == nil { 77 | return errgo.Newf("no IP address found in context") 78 | } 79 | ip := net.ParseIP(args) 80 | if ip == nil { 81 | return errgo.Newf("cannot parse IP address in caveat") 82 | } 83 | if req.RemoteAddr == "" { 84 | return errgo.Newf("client has no remote address") 85 | } 86 | reqIP, err := requestIPAddr(req) 87 | if err != nil { 88 | return errgo.Mask(err) 89 | } 90 | if !reqIP.Equal(ip) { 91 | return errgo.Newf("client IP address mismatch, got %s", reqIP) 92 | } 93 | return nil 94 | } 95 | 96 | // clientOriginCheck implements the Origin header checker 97 | // for an HTTP request. 98 | func clientOriginCheck(ctx context.Context, cond, args string) error { 99 | req := requestFromContext(ctx) 100 | if req == nil { 101 | return errgo.Newf("no origin found in context") 102 | } 103 | // Note that web browsers may not provide the origin header when it's 104 | // not a cross-site request with a GET method. There's nothing we 105 | // can do about that, so just allow all requests with an empty origin. 106 | if reqOrigin := req.Header.Get("Origin"); reqOrigin != "" && reqOrigin != args { 107 | return errgo.Newf("request has invalid Origin header; got %q", reqOrigin) 108 | } 109 | return nil 110 | } 111 | 112 | // SameClientIPAddrCaveat returns a caveat that will check that 113 | // the remote IP address is the same as that in the given HTTP request. 114 | func SameClientIPAddrCaveat(req *http.Request) checkers.Caveat { 115 | if req.RemoteAddr == "" { 116 | return checkers.ErrorCaveatf("client has no remote IP address") 117 | } 118 | ip, err := requestIPAddr(req) 119 | if err != nil { 120 | return checkers.ErrorCaveatf("%v", err) 121 | } 122 | return ClientIPAddrCaveat(ip) 123 | } 124 | 125 | // ClientIPAddrCaveat returns a caveat that will check whether the 126 | // client's IP address is as provided. 127 | func ClientIPAddrCaveat(addr net.IP) checkers.Caveat { 128 | if len(addr) != net.IPv4len && len(addr) != net.IPv6len { 129 | return checkers.ErrorCaveatf("bad IP address %d", []byte(addr)) 130 | } 131 | return httpCaveat(CondClientIPAddr, addr.String()) 132 | } 133 | 134 | // ClientOriginCaveat returns a caveat that will check whether the 135 | // client's Origin header in its HTTP request is as provided. 136 | func ClientOriginCaveat(origin string) checkers.Caveat { 137 | return httpCaveat(CondClientOrigin, origin) 138 | } 139 | 140 | func httpCaveat(cond, arg string) checkers.Caveat { 141 | return checkers.Caveat{ 142 | Condition: checkers.Condition(cond, arg), 143 | Namespace: CheckersNamespace, 144 | } 145 | } 146 | 147 | func requestIPAddr(req *http.Request) (net.IP, error) { 148 | reqHost, _, err := net.SplitHostPort(req.RemoteAddr) 149 | if err != nil { 150 | return nil, errgo.Newf("cannot parse host port in remote address: %v", err) 151 | } 152 | ip := net.ParseIP(reqHost) 153 | if ip == nil { 154 | return nil, errgo.Newf("invalid IP address in remote address %q", req.RemoteAddr) 155 | } 156 | return ip, nil 157 | } 158 | -------------------------------------------------------------------------------- /httpbakery/checkers_test.go: -------------------------------------------------------------------------------- 1 | package httpbakery_test 2 | 3 | import ( 4 | "net" 5 | "net/http" 6 | "testing" 7 | 8 | qt "github.com/frankban/quicktest" 9 | "gopkg.in/errgo.v1" 10 | 11 | "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/checkers" 12 | "github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery" 13 | ) 14 | 15 | type checkTest struct { 16 | caveat checkers.Caveat 17 | expectError string 18 | expectCause func(err error) bool 19 | } 20 | 21 | func caveatWithCondition(cond string) checkers.Caveat { 22 | return checkers.Caveat{ 23 | Condition: cond, 24 | } 25 | } 26 | 27 | var checkerTests = []struct { 28 | about string 29 | req *http.Request 30 | checks []checkTest 31 | }{{ 32 | about: "no host name declared", 33 | req: &http.Request{}, 34 | checks: []checkTest{{ 35 | caveat: httpbakery.ClientIPAddrCaveat(net.IP{0, 0, 0, 0}), 36 | expectError: `caveat "http:client-ip-addr 0.0.0.0" not satisfied: client has no remote address`, 37 | }, { 38 | caveat: httpbakery.ClientIPAddrCaveat(net.IP{127, 0, 0, 1}), 39 | expectError: `caveat "http:client-ip-addr 127.0.0.1" not satisfied: client has no remote address`, 40 | }, { 41 | caveat: caveatWithCondition("http:client-ip-addr badip"), 42 | expectError: `caveat "http:client-ip-addr badip" not satisfied: cannot parse IP address in caveat`, 43 | }}, 44 | }, { 45 | about: "IPv4 host name declared", 46 | req: &http.Request{ 47 | RemoteAddr: "127.0.0.1:1234", 48 | }, 49 | checks: []checkTest{{ 50 | caveat: httpbakery.ClientIPAddrCaveat(net.IP{127, 0, 0, 1}), 51 | }, { 52 | caveat: httpbakery.ClientIPAddrCaveat(net.IP{127, 0, 0, 1}.To16()), 53 | }, { 54 | caveat: caveatWithCondition("http:client-ip-addr ::ffff:7f00:1"), 55 | }, { 56 | caveat: httpbakery.ClientIPAddrCaveat(net.IP{127, 0, 0, 2}), 57 | expectError: `caveat "http:client-ip-addr 127.0.0.2" not satisfied: client IP address mismatch, got 127.0.0.1`, 58 | }, { 59 | caveat: httpbakery.ClientIPAddrCaveat(net.ParseIP("2001:4860:0:2001::68")), 60 | expectError: `caveat "http:client-ip-addr 2001:4860:0:2001::68" not satisfied: client IP address mismatch, got 127.0.0.1`, 61 | }}, 62 | }, { 63 | about: "IPv6 host name declared", 64 | req: &http.Request{ 65 | RemoteAddr: "[2001:4860:0:2001::68]:1234", 66 | }, 67 | checks: []checkTest{{ 68 | caveat: httpbakery.ClientIPAddrCaveat(net.ParseIP("2001:4860:0:2001::68")), 69 | }, { 70 | caveat: caveatWithCondition("http:client-ip-addr 2001:4860:0:2001:0::68"), 71 | }, { 72 | caveat: httpbakery.ClientIPAddrCaveat(net.ParseIP("2001:4860:0:2001::69")), 73 | expectError: `caveat "http:client-ip-addr 2001:4860:0:2001::69" not satisfied: client IP address mismatch, got 2001:4860:0:2001::68`, 74 | }, { 75 | caveat: httpbakery.ClientIPAddrCaveat(net.ParseIP("127.0.0.1")), 76 | expectError: `caveat "http:client-ip-addr 127.0.0.1" not satisfied: client IP address mismatch, got 2001:4860:0:2001::68`, 77 | }}, 78 | }, { 79 | about: "same client address, ipv4 request address", 80 | req: &http.Request{ 81 | RemoteAddr: "127.0.0.1:1324", 82 | }, 83 | checks: []checkTest{{ 84 | caveat: httpbakery.SameClientIPAddrCaveat(&http.Request{ 85 | RemoteAddr: "127.0.0.1:1234", 86 | }), 87 | }, { 88 | caveat: httpbakery.SameClientIPAddrCaveat(&http.Request{ 89 | RemoteAddr: "[::ffff:7f00:1]:1235", 90 | }), 91 | }, { 92 | caveat: httpbakery.SameClientIPAddrCaveat(&http.Request{ 93 | RemoteAddr: "127.0.0.2:1234", 94 | }), 95 | expectError: `caveat "http:client-ip-addr 127.0.0.2" not satisfied: client IP address mismatch, got 127.0.0.1`, 96 | }, { 97 | caveat: httpbakery.SameClientIPAddrCaveat(&http.Request{ 98 | RemoteAddr: "[::ffff:7f00:2]:1235", 99 | }), 100 | expectError: `caveat "http:client-ip-addr 127.0.0.2" not satisfied: client IP address mismatch, got 127.0.0.1`, 101 | }, { 102 | caveat: httpbakery.SameClientIPAddrCaveat(&http.Request{}), 103 | expectError: `caveat "error client has no remote IP address" not satisfied: bad caveat`, 104 | }, { 105 | caveat: httpbakery.SameClientIPAddrCaveat(&http.Request{ 106 | RemoteAddr: "bad", 107 | }), 108 | expectError: `caveat "error cannot parse host port in remote address: .*" not satisfied: bad caveat`, 109 | }, { 110 | caveat: httpbakery.SameClientIPAddrCaveat(&http.Request{ 111 | RemoteAddr: "bad:56", 112 | }), 113 | expectError: `caveat "error invalid IP address in remote address \\"bad:56\\"" not satisfied: bad caveat`, 114 | }}, 115 | }, { 116 | about: "same client address, ipv6 request address", 117 | req: &http.Request{ 118 | RemoteAddr: "[2001:4860:0:2001:0::68]:1235", 119 | }, 120 | checks: []checkTest{{ 121 | caveat: httpbakery.SameClientIPAddrCaveat(&http.Request{ 122 | RemoteAddr: "[2001:4860:0:2001:0::68]:1234", 123 | }), 124 | }, { 125 | caveat: httpbakery.SameClientIPAddrCaveat(&http.Request{ 126 | RemoteAddr: "127.0.0.2:1234", 127 | }), 128 | expectError: `caveat "http:client-ip-addr 127.0.0.2" not satisfied: client IP address mismatch, got 2001:4860:0:2001::68`, 129 | }}, 130 | }, { 131 | about: "request with no origin", 132 | req: &http.Request{}, 133 | checks: []checkTest{{ 134 | caveat: httpbakery.ClientOriginCaveat(""), 135 | }, { 136 | caveat: httpbakery.ClientOriginCaveat("somewhere"), 137 | }}, 138 | }, { 139 | about: "request with origin", 140 | req: &http.Request{ 141 | Header: http.Header{ 142 | "Origin": {"somewhere"}, 143 | }, 144 | }, 145 | checks: []checkTest{{ 146 | caveat: httpbakery.ClientOriginCaveat(""), 147 | expectError: `caveat "http:origin" not satisfied: request has invalid Origin header; got "somewhere"`, 148 | }, { 149 | caveat: httpbakery.ClientOriginCaveat("somewhere"), 150 | }}, 151 | }} 152 | 153 | func TestCheckers(t *testing.T) { 154 | c := qt.New(t) 155 | checker := httpbakery.NewChecker() 156 | for i, test := range checkerTests { 157 | c.Logf("test %d: %s", i, test.about) 158 | ctx := httpbakery.ContextWithRequest(testContext, test.req) 159 | for j, check := range test.checks { 160 | c.Logf("\tcheck %d", j) 161 | 162 | err := checker.CheckFirstPartyCaveat(ctx, checker.Namespace().ResolveCaveat(check.caveat).Condition) 163 | if check.expectError != "" { 164 | c.Assert(err, qt.ErrorMatches, check.expectError) 165 | if check.expectCause == nil { 166 | check.expectCause = errgo.Any 167 | } 168 | c.Assert(check.expectCause(errgo.Cause(err)), qt.Equals, true) 169 | } else { 170 | c.Assert(err, qt.IsNil) 171 | } 172 | } 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /httpbakery/context_go17.go: -------------------------------------------------------------------------------- 1 | // +build go1.7 2 | 3 | package httpbakery 4 | 5 | import ( 6 | "context" 7 | "net/http" 8 | ) 9 | 10 | func contextFromRequest(req *http.Request) context.Context { 11 | return req.Context() 12 | } 13 | -------------------------------------------------------------------------------- /httpbakery/context_prego17.go: -------------------------------------------------------------------------------- 1 | // +build !go1.7 2 | 3 | package httpbakery 4 | 5 | import ( 6 | "context" 7 | "net/http" 8 | ) 9 | 10 | func contextFromRequest(req *http.Request) context.Context { 11 | return context.Background() 12 | } 13 | -------------------------------------------------------------------------------- /httpbakery/dischargeclient_generated.go: -------------------------------------------------------------------------------- 1 | // The code in this file was automatically generated by running httprequest-generate-client. 2 | // DO NOT EDIT 3 | 4 | package httpbakery 5 | 6 | import ( 7 | "context" 8 | 9 | "gopkg.in/httprequest.v1" 10 | ) 11 | 12 | type dischargeClient struct { 13 | Client httprequest.Client 14 | } 15 | 16 | // Discharge discharges a third party caveat. 17 | func (c *dischargeClient) Discharge(ctx context.Context, p *dischargeRequest) (*dischargeResponse, error) { 18 | var r *dischargeResponse 19 | err := c.Client.Call(ctx, p, &r) 20 | return r, err 21 | } 22 | 23 | // DischargeInfo returns information on the discharger. 24 | func (c *dischargeClient) DischargeInfo(ctx context.Context, p *dischargeInfoRequest) (dischargeInfoResponse, error) { 25 | var r dischargeInfoResponse 26 | err := c.Client.Call(ctx, p, &r) 27 | return r, err 28 | } 29 | 30 | // PublicKey returns the public key of the discharge service. 31 | func (c *dischargeClient) PublicKey(ctx context.Context, p *publicKeyRequest) (publicKeyResponse, error) { 32 | var r publicKeyResponse 33 | err := c.Client.Call(ctx, p, &r) 34 | return r, err 35 | } 36 | -------------------------------------------------------------------------------- /httpbakery/export_test.go: -------------------------------------------------------------------------------- 1 | package httpbakery 2 | 3 | type PublicKeyResponse publicKeyResponse 4 | 5 | const MaxDischargeRetries = maxDischargeRetries 6 | 7 | var LegacyGetInteractionMethods = legacyGetInteractionMethods 8 | -------------------------------------------------------------------------------- /httpbakery/form/form.go: -------------------------------------------------------------------------------- 1 | // Package form enables interactive login without using a web browser. 2 | package form 3 | 4 | import ( 5 | "context" 6 | "net/url" 7 | 8 | "golang.org/x/net/publicsuffix" 9 | "gopkg.in/errgo.v1" 10 | "gopkg.in/httprequest.v1" 11 | "gopkg.in/juju/environschema.v1" 12 | "gopkg.in/juju/environschema.v1/form" 13 | 14 | "github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery" 15 | ) 16 | 17 | /* 18 | PROTOCOL 19 | 20 | A form login works as follows: 21 | 22 | Client Login Service 23 | | | 24 | | Discharge request | 25 | |----------------------------------->| 26 | | | 27 | | Interaction-required error with | 28 | | "form" entry with formURL. | 29 | |<-----------------------------------| 30 | | | 31 | | GET "form" URL | 32 | |----------------------------------->| 33 | | | 34 | | Schema definition | 35 | |<-----------------------------------| 36 | | | 37 | +-------------+ | 38 | | Client | | 39 | | Interaction | | 40 | +-------------+ | 41 | | | 42 | | POST data to "form" URL | 43 | |----------------------------------->| 44 | | | 45 | | Form login response | 46 | | with discharge token | 47 | |<-----------------------------------| 48 | | | 49 | | Discharge request with | 50 | | discharge token. | 51 | |----------------------------------->| 52 | | | 53 | | Discharge macaroon | 54 | |<-----------------------------------| 55 | 56 | 57 | The schema is provided as a environschema.Fields object. It is the 58 | client's responsibility to interpret the schema and present it to the 59 | user. 60 | */ 61 | 62 | const ( 63 | // InteractionMethod is the methodURLs key 64 | // used for a URL that can be used for form-based 65 | // interaction. 66 | InteractionMethod = "form" 67 | ) 68 | 69 | // SchemaResponse contains the message expected in response to the schema 70 | // request. 71 | type SchemaResponse struct { 72 | Schema environschema.Fields `json:"schema"` 73 | } 74 | 75 | // InteractionInfo holds the information expected in 76 | // the form interaction entry in an interaction-required 77 | // error. 78 | type InteractionInfo struct { 79 | URL string `json:"url"` 80 | } 81 | 82 | // LoginRequest is a request to perform a login using the provided form. 83 | type LoginRequest struct { 84 | httprequest.Route `httprequest:"POST"` 85 | Body LoginBody `httprequest:",body"` 86 | } 87 | 88 | // LoginBody holds the body of a form login request. 89 | type LoginBody struct { 90 | Form map[string]interface{} `json:"form"` 91 | } 92 | 93 | type LoginResponse struct { 94 | Token *httpbakery.DischargeToken `json:"token"` 95 | } 96 | 97 | // Interactor implements httpbakery.Interactor 98 | // by providing form-based interaction. 99 | type Interactor struct { 100 | // Filler holds the form filler that will be used when 101 | // form-based interaction is required. 102 | Filler form.Filler 103 | } 104 | 105 | // Kind implements httpbakery.Interactor.Kind. 106 | func (i Interactor) Kind() string { 107 | return InteractionMethod 108 | } 109 | 110 | // Interact implements httpbakery.Interactor.Interact. 111 | func (i Interactor) Interact(ctx context.Context, client *httpbakery.Client, location string, interactionRequiredErr *httpbakery.Error) (*httpbakery.DischargeToken, error) { 112 | var p InteractionInfo 113 | if err := interactionRequiredErr.InteractionMethod(InteractionMethod, &p); err != nil { 114 | return nil, errgo.Mask(err) 115 | } 116 | if p.URL == "" { 117 | return nil, errgo.Newf("no URL found in form information") 118 | } 119 | schemaURL, err := relativeURL(location, p.URL) 120 | if err != nil { 121 | return nil, errgo.Notef(err, "invalid url %q", p.URL) 122 | } 123 | httpReqClient := &httprequest.Client{ 124 | Doer: client, 125 | } 126 | var s SchemaResponse 127 | if err := httpReqClient.Get(ctx, schemaURL.String(), &s); err != nil { 128 | return nil, errgo.Notef(err, "cannot get schema") 129 | } 130 | if len(s.Schema) == 0 { 131 | return nil, errgo.Newf("invalid schema: no fields found") 132 | } 133 | host, err := publicsuffix.EffectiveTLDPlusOne(schemaURL.Host) 134 | if err != nil { 135 | host = schemaURL.Host 136 | } 137 | formValues, err := i.Filler.Fill(form.Form{ 138 | Title: "Log in to " + host, 139 | Fields: s.Schema, 140 | }) 141 | if err != nil { 142 | return nil, errgo.NoteMask(err, "cannot handle form", errgo.Any) 143 | } 144 | lr := LoginRequest{ 145 | Body: LoginBody{ 146 | Form: formValues, 147 | }, 148 | } 149 | var lresp LoginResponse 150 | if err := httpReqClient.CallURL(ctx, schemaURL.String(), &lr, &lresp); err != nil { 151 | return nil, errgo.Notef(err, "cannot submit form") 152 | } 153 | if lresp.Token == nil { 154 | return nil, errgo.Newf("no token found in form response") 155 | } 156 | return lresp.Token, nil 157 | } 158 | 159 | // relativeURL returns newPath relative to an original URL. 160 | func relativeURL(base, new string) (*url.URL, error) { 161 | if new == "" { 162 | return nil, errgo.Newf("empty URL") 163 | } 164 | baseURL, err := url.Parse(base) 165 | if err != nil { 166 | return nil, errgo.Notef(err, "cannot parse URL") 167 | } 168 | newURL, err := url.Parse(new) 169 | if err != nil { 170 | return nil, errgo.Notef(err, "cannot parse URL") 171 | } 172 | return baseURL.ResolveReference(newURL), nil 173 | } 174 | -------------------------------------------------------------------------------- /httpbakery/keyring.go: -------------------------------------------------------------------------------- 1 | package httpbakery 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "net/url" 7 | 8 | "gopkg.in/errgo.v1" 9 | "gopkg.in/httprequest.v1" 10 | 11 | "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" 12 | ) 13 | 14 | var _ bakery.ThirdPartyLocator = (*ThirdPartyLocator)(nil) 15 | 16 | // NewThirdPartyLocator returns a new third party 17 | // locator that uses the given client to find 18 | // information about third parties and 19 | // uses the given cache as a backing. 20 | // 21 | // If cache is nil, a new cache will be created. 22 | // 23 | // If client is nil, http.DefaultClient will be used. 24 | func NewThirdPartyLocator(client httprequest.Doer, cache *bakery.ThirdPartyStore) *ThirdPartyLocator { 25 | if cache == nil { 26 | cache = bakery.NewThirdPartyStore() 27 | } 28 | if client == nil { 29 | client = http.DefaultClient 30 | } 31 | return &ThirdPartyLocator{ 32 | client: client, 33 | cache: cache, 34 | } 35 | } 36 | 37 | // AllowInsecureThirdPartyLocator holds whether ThirdPartyLocator allows 38 | // insecure HTTP connections for fetching third party information. 39 | // It is provided for testing purposes and should not be used 40 | // in production code. 41 | var AllowInsecureThirdPartyLocator = false 42 | 43 | // ThirdPartyLocator represents locator that can interrogate 44 | // third party discharge services for information. By default it refuses 45 | // to use insecure URLs. 46 | type ThirdPartyLocator struct { 47 | client httprequest.Doer 48 | allowInsecure bool 49 | cache *bakery.ThirdPartyStore 50 | } 51 | 52 | // AllowInsecure allows insecure URLs. This can be useful 53 | // for testing purposes. See also AllowInsecureThirdPartyLocator. 54 | func (kr *ThirdPartyLocator) AllowInsecure() { 55 | kr.allowInsecure = true 56 | } 57 | 58 | // ThirdPartyLocator implements bakery.ThirdPartyLocator 59 | // by first looking in the backing cache and, if that fails, 60 | // making an HTTP request to find the information associated 61 | // with the given discharge location. 62 | // 63 | // It refuses to fetch information from non-HTTPS URLs. 64 | func (kr *ThirdPartyLocator) ThirdPartyInfo(ctx context.Context, loc string) (bakery.ThirdPartyInfo, error) { 65 | // If the cache has an entry in, we can use it regardless of URL scheme. 66 | // This allows entries for notionally insecure URLs to be added by other means (for 67 | // example via a config file). 68 | info, err := kr.cache.ThirdPartyInfo(ctx, loc) 69 | if err == nil { 70 | return info, nil 71 | } 72 | u, err := url.Parse(loc) 73 | if err != nil { 74 | return bakery.ThirdPartyInfo{}, errgo.Notef(err, "invalid discharge URL %q", loc) 75 | } 76 | if u.Scheme != "https" && !kr.allowInsecure && !AllowInsecureThirdPartyLocator { 77 | return bakery.ThirdPartyInfo{}, errgo.Newf("untrusted discharge URL %q", loc) 78 | } 79 | info, err = ThirdPartyInfoForLocation(ctx, kr.client, loc) 80 | if err != nil { 81 | return bakery.ThirdPartyInfo{}, errgo.Mask(err) 82 | } 83 | kr.cache.AddInfo(loc, info) 84 | return info, nil 85 | } 86 | 87 | // ThirdPartyInfoForLocation returns information on the third party 88 | // discharge server running at the given location URL. Note that this is 89 | // insecure if an http: URL scheme is used. If client is nil, 90 | // http.DefaultClient will be used. 91 | func ThirdPartyInfoForLocation(ctx context.Context, client httprequest.Doer, url string) (bakery.ThirdPartyInfo, error) { 92 | dclient := newDischargeClient(url, client) 93 | info, err := dclient.DischargeInfo(ctx, &dischargeInfoRequest{}) 94 | if err == nil { 95 | return bakery.ThirdPartyInfo{ 96 | PublicKey: *info.PublicKey, 97 | Version: info.Version, 98 | }, nil 99 | } 100 | derr, ok := errgo.Cause(err).(*httprequest.DecodeResponseError) 101 | if !ok || derr.Response.StatusCode != http.StatusNotFound { 102 | return bakery.ThirdPartyInfo{}, errgo.Mask(err) 103 | } 104 | // The new endpoint isn't there, so try the old one. 105 | pkResp, err := dclient.PublicKey(ctx, &publicKeyRequest{}) 106 | if err != nil { 107 | return bakery.ThirdPartyInfo{}, errgo.Mask(err) 108 | } 109 | return bakery.ThirdPartyInfo{ 110 | PublicKey: *pkResp.PublicKey, 111 | Version: bakery.Version1, 112 | }, nil 113 | } 114 | -------------------------------------------------------------------------------- /httpbakery/keyring_test.go: -------------------------------------------------------------------------------- 1 | package httpbakery_test 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "net/http/httptest" 7 | "net/http/httputil" 8 | "net/url" 9 | "sync" 10 | "testing" 11 | 12 | qt "github.com/frankban/quicktest" 13 | "gopkg.in/errgo.v1" 14 | "gopkg.in/httprequest.v1" 15 | 16 | "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" 17 | "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakerytest" 18 | "github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery" 19 | ) 20 | 21 | func TestCachePrepopulated(t *testing.T) { 22 | c := qt.New(t) 23 | cache := bakery.NewThirdPartyStore() 24 | key, err := bakery.GenerateKey() 25 | c.Assert(err, qt.IsNil) 26 | expectInfo := bakery.ThirdPartyInfo{ 27 | PublicKey: key.Public, 28 | Version: bakery.LatestVersion, 29 | } 30 | cache.AddInfo("https://0.1.2.3/", expectInfo) 31 | kr := httpbakery.NewThirdPartyLocator(nil, cache) 32 | info, err := kr.ThirdPartyInfo(testContext, "https://0.1.2.3/") 33 | c.Assert(err, qt.IsNil) 34 | c.Assert(info, qt.DeepEquals, expectInfo) 35 | } 36 | 37 | func TestCachePrepopulatedInsecure(t *testing.T) { 38 | c := qt.New(t) 39 | // We allow an insecure URL in a prepopulated cache. 40 | cache := bakery.NewThirdPartyStore() 41 | key, err := bakery.GenerateKey() 42 | c.Assert(err, qt.Equals, nil) 43 | expectInfo := bakery.ThirdPartyInfo{ 44 | PublicKey: key.Public, 45 | Version: bakery.LatestVersion, 46 | } 47 | cache.AddInfo("http://0.1.2.3/", expectInfo) 48 | kr := httpbakery.NewThirdPartyLocator(nil, cache) 49 | info, err := kr.ThirdPartyInfo(testContext, "http://0.1.2.3/") 50 | c.Assert(err, qt.Equals, nil) 51 | c.Assert(info, qt.DeepEquals, expectInfo) 52 | } 53 | 54 | func TestCacheMiss(t *testing.T) { 55 | c := qt.New(t) 56 | d := bakerytest.NewDischarger(nil) 57 | defer d.Close() 58 | kr := httpbakery.NewThirdPartyLocator(nil, nil) 59 | 60 | expectInfo := bakery.ThirdPartyInfo{ 61 | PublicKey: d.Key.Public, 62 | Version: bakery.LatestVersion, 63 | } 64 | location := d.Location() 65 | info, err := kr.ThirdPartyInfo(testContext, location) 66 | c.Assert(err, qt.IsNil) 67 | c.Assert(info, qt.DeepEquals, expectInfo) 68 | 69 | // Close down the service and make sure that 70 | // the key is cached. 71 | d.Close() 72 | 73 | info, err = kr.ThirdPartyInfo(testContext, location) 74 | c.Assert(err, qt.IsNil) 75 | c.Assert(info, qt.DeepEquals, expectInfo) 76 | } 77 | 78 | func TestInsecureURL(t *testing.T) { 79 | c := qt.New(t) 80 | // Set up a discharger with an non-HTTPS access point. 81 | d := bakerytest.NewDischarger(nil) 82 | defer d.Close() 83 | httpsDischargeURL, err := url.Parse(d.Location()) 84 | c.Assert(err, qt.IsNil) 85 | 86 | srv := httptest.NewServer(httputil.NewSingleHostReverseProxy(httpsDischargeURL)) 87 | defer srv.Close() 88 | 89 | // Check that we are refused because it's an insecure URL. 90 | kr := httpbakery.NewThirdPartyLocator(nil, nil) 91 | info, err := kr.ThirdPartyInfo(testContext, srv.URL) 92 | c.Assert(err, qt.ErrorMatches, `untrusted discharge URL "http://.*"`) 93 | c.Assert(info, qt.DeepEquals, bakery.ThirdPartyInfo{}) 94 | 95 | // Check that it does work when we've enabled AllowInsecure. 96 | kr.AllowInsecure() 97 | info, err = kr.ThirdPartyInfo(testContext, srv.URL) 98 | c.Assert(err, qt.IsNil) 99 | c.Assert(info, qt.DeepEquals, bakery.ThirdPartyInfo{ 100 | PublicKey: d.Key.Public, 101 | Version: bakery.LatestVersion, 102 | }) 103 | } 104 | 105 | func TestConcurrentThirdPartyInfo(t *testing.T) { 106 | c := qt.New(t) 107 | // This test is designed to fail only if run with the race detector 108 | // enabled. 109 | d := bakerytest.NewDischarger(nil) 110 | defer d.Close() 111 | kr := httpbakery.NewThirdPartyLocator(nil, nil) 112 | var wg sync.WaitGroup 113 | for i := 0; i < 2; i++ { 114 | wg.Add(1) 115 | go func() { 116 | _, err := kr.ThirdPartyInfo(testContext, d.Location()) 117 | c.Check(err, qt.IsNil) 118 | defer wg.Done() 119 | }() 120 | } 121 | wg.Wait() 122 | } 123 | 124 | func TestCustomHTTPClient(t *testing.T) { 125 | c := qt.New(t) 126 | client := &http.Client{ 127 | Transport: errorTransport{}, 128 | } 129 | kr := httpbakery.NewThirdPartyLocator(client, nil) 130 | info, err := kr.ThirdPartyInfo(testContext, "https://0.1.2.3/") 131 | c.Assert(err, qt.ErrorMatches, `(Get|GET) ["]?https://0.1.2.3/discharge/info["]?: custom round trip error`) 132 | c.Assert(info, qt.DeepEquals, bakery.ThirdPartyInfo{}) 133 | } 134 | 135 | func TestThirdPartyInfoForLocation(t *testing.T) { 136 | c := qt.New(t) 137 | d := bakerytest.NewDischarger(nil) 138 | defer d.Close() 139 | client := httpbakery.NewHTTPClient() 140 | info, err := httpbakery.ThirdPartyInfoForLocation(testContext, client, d.Location()) 141 | c.Assert(err, qt.IsNil) 142 | expectedInfo := bakery.ThirdPartyInfo{ 143 | PublicKey: d.Key.Public, 144 | Version: bakery.LatestVersion, 145 | } 146 | c.Assert(info, qt.DeepEquals, expectedInfo) 147 | 148 | // Check that it works with client==nil. 149 | info, err = httpbakery.ThirdPartyInfoForLocation(testContext, nil, d.Location()) 150 | c.Assert(err, qt.IsNil) 151 | c.Assert(info, qt.DeepEquals, expectedInfo) 152 | } 153 | 154 | func TestThirdPartyInfoForLocationWrongURL(t *testing.T) { 155 | c := qt.New(t) 156 | client := httpbakery.NewHTTPClient() 157 | _, err := httpbakery.ThirdPartyInfoForLocation(testContext, client, "http://localhost:0") 158 | c.Logf("%v", errgo.Details(err)) 159 | c.Assert(err, qt.ErrorMatches, 160 | `(Get|GET) ["]?http://localhost:0/discharge/info["]?: dial tcp (127.0.0.1|\[::1\]):0: .*connection refused`) 161 | } 162 | 163 | func TestThirdPartyInfoForLocationReturnsInvalidJSON(t *testing.T) { 164 | c := qt.New(t) 165 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 166 | fmt.Fprintln(w, "BADJSON") 167 | })) 168 | defer ts.Close() 169 | client := httpbakery.NewHTTPClient() 170 | _, err := httpbakery.ThirdPartyInfoForLocation(testContext, client, ts.URL) 171 | c.Assert(err, qt.ErrorMatches, 172 | fmt.Sprintf(`Get ["]?http://.*/discharge/info["]?: unexpected content type text/plain; want application/json; content: BADJSON`)) 173 | } 174 | 175 | func TestThirdPartyInfoForLocationReturnsStatusInternalServerError(t *testing.T) { 176 | c := qt.New(t) 177 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 178 | w.WriteHeader(http.StatusInternalServerError) 179 | })) 180 | defer ts.Close() 181 | client := httpbakery.NewHTTPClient() 182 | _, err := httpbakery.ThirdPartyInfoForLocation(testContext, client, ts.URL) 183 | c.Assert(err, qt.ErrorMatches, `Get .*/discharge/info: cannot unmarshal error response \(status 500 Internal Server Error\): unexpected content type .*`) 184 | } 185 | 186 | func TestThirdPartyInfoForLocationFallbackToOldVersion(t *testing.T) { 187 | c := qt.New(t) 188 | // Start a bakerytest discharger so we benefit from its TLS-verification-skip logic. 189 | d := bakerytest.NewDischarger(nil) 190 | defer d.Close() 191 | 192 | key, err := bakery.GenerateKey() 193 | c.Assert(err, qt.IsNil) 194 | 195 | // Start a server which serves the publickey endpoint only. 196 | mux := http.NewServeMux() 197 | server := httptest.NewTLSServer(mux) 198 | mux.HandleFunc("/publickey", func(w http.ResponseWriter, req *http.Request) { 199 | c.Check(req.Method, qt.Equals, "GET") 200 | httprequest.WriteJSON(w, http.StatusOK, &httpbakery.PublicKeyResponse{ 201 | PublicKey: &key.Public, 202 | }) 203 | }) 204 | info, err := httpbakery.ThirdPartyInfoForLocation(testContext, httpbakery.NewHTTPClient(), server.URL) 205 | c.Assert(err, qt.IsNil) 206 | expectedInfo := bakery.ThirdPartyInfo{ 207 | PublicKey: key.Public, 208 | Version: bakery.Version1, 209 | } 210 | c.Assert(info, qt.DeepEquals, expectedInfo) 211 | } 212 | 213 | type errorTransport struct{} 214 | 215 | func (errorTransport) RoundTrip(req *http.Request) (*http.Response, error) { 216 | return nil, errgo.New("custom round trip error") 217 | } 218 | -------------------------------------------------------------------------------- /httpbakery/oven.go: -------------------------------------------------------------------------------- 1 | package httpbakery 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "time" 7 | 8 | "gopkg.in/errgo.v1" 9 | 10 | "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" 11 | "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/checkers" 12 | ) 13 | 14 | // Oven is like bakery.Oven except it provides a method for 15 | // translating errors returned by bakery.AuthChecker into 16 | // errors suitable for passing to WriteError. 17 | type Oven struct { 18 | // Oven holds the bakery Oven used to create 19 | // new macaroons to put in discharge-required errors. 20 | *bakery.Oven 21 | 22 | // AuthnExpiry holds the expiry time of macaroons that 23 | // are created for authentication. As these are generally 24 | // applicable to all endpoints in an API, this is usually 25 | // longer than AuthzExpiry. If this is zero, DefaultAuthnExpiry 26 | // will be used. 27 | AuthnExpiry time.Duration 28 | 29 | // AuthzExpiry holds the expiry time of macaroons that are 30 | // created for authorization. As these are generally applicable 31 | // to specific operations, they generally don't need 32 | // a long lifespan, so this is usually shorter than AuthnExpiry. 33 | // If this is zero, DefaultAuthzExpiry will be used. 34 | AuthzExpiry time.Duration 35 | } 36 | 37 | // Default expiry times for macaroons created by Oven.Error. 38 | const ( 39 | DefaultAuthnExpiry = 7 * 24 * time.Hour 40 | DefaultAuthzExpiry = 5 * time.Minute 41 | ) 42 | 43 | // Error processes an error as returned from bakery.AuthChecker 44 | // into an error suitable for returning as a response to req 45 | // with WriteError. 46 | // 47 | // Specifically, it translates bakery.ErrPermissionDenied into 48 | // ErrPermissionDenied and bakery.DischargeRequiredError 49 | // into an Error with an ErrDischargeRequired code, using 50 | // oven.Oven to mint the macaroon in it. 51 | func (oven *Oven) Error(ctx context.Context, req *http.Request, err error) error { 52 | cause := errgo.Cause(err) 53 | if cause == bakery.ErrPermissionDenied { 54 | return errgo.WithCausef(err, ErrPermissionDenied, "") 55 | } 56 | derr, ok := cause.(*bakery.DischargeRequiredError) 57 | if !ok { 58 | return errgo.Mask(err) 59 | } 60 | // TODO it's possible to have more than two levels here - think 61 | // about some naming scheme for the cookies that allows that. 62 | expiryDuration := oven.AuthzExpiry 63 | if expiryDuration == 0 { 64 | expiryDuration = DefaultAuthzExpiry 65 | } 66 | cookieName := "authz" 67 | if derr.ForAuthentication { 68 | // Authentication macaroons are a bit different, so use 69 | // a different cookie name so both can be presented together. 70 | cookieName = "authn" 71 | expiryDuration = oven.AuthnExpiry 72 | if expiryDuration == 0 { 73 | expiryDuration = DefaultAuthnExpiry 74 | } 75 | } 76 | m, err := oven.Oven.NewMacaroon(ctx, RequestVersion(req), derr.Caveats, derr.Ops...) 77 | if err != nil { 78 | return errgo.Notef(err, "cannot mint new macaroon") 79 | } 80 | if err := m.AddCaveat(ctx, checkers.TimeBeforeCaveat(time.Now().Add(expiryDuration)), nil, nil); err != nil { 81 | return errgo.Notef(err, "cannot add time-before caveat") 82 | } 83 | return NewDischargeRequiredError(DischargeRequiredErrorParams{ 84 | Macaroon: m, 85 | CookieNameSuffix: cookieName, 86 | Request: req, 87 | }) 88 | } 89 | -------------------------------------------------------------------------------- /httpbakery/oven_test.go: -------------------------------------------------------------------------------- 1 | package httpbakery_test 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io/ioutil" 7 | "net/http" 8 | "net/http/httptest" 9 | "testing" 10 | "time" 11 | 12 | qt "github.com/frankban/quicktest" 13 | "gopkg.in/errgo.v1" 14 | "gopkg.in/httprequest.v1" 15 | 16 | "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" 17 | "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/checkers" 18 | "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/identchecker" 19 | "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakerytest" 20 | "github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery" 21 | ) 22 | 23 | func TestOvenWithAuthnMacaroon(t *testing.T) { 24 | c := qt.New(t) 25 | discharger := newTestIdentityServer() 26 | defer discharger.Close() 27 | 28 | key, err := bakery.GenerateKey() 29 | if err != nil { 30 | panic(err) 31 | } 32 | b := identchecker.NewBakery(identchecker.BakeryParams{ 33 | Location: "here", 34 | Locator: discharger, 35 | Key: key, 36 | Checker: httpbakery.NewChecker(), 37 | IdentityClient: discharger, 38 | }) 39 | expectedExpiry := time.Hour 40 | oven := &httpbakery.Oven{ 41 | Oven: b.Oven, 42 | AuthnExpiry: expectedExpiry, 43 | AuthzExpiry: 5 * time.Minute, 44 | } 45 | errorCalled := 0 46 | handler := httpReqServer.HandleErrors(func(p httprequest.Params) error { 47 | if _, err := b.Checker.Auth(httpbakery.RequestMacaroons(p.Request)...).Allow(p.Context, identchecker.LoginOp); err != nil { 48 | errorCalled++ 49 | return oven.Error(testContext, p.Request, err) 50 | } 51 | fmt.Fprintf(p.Response, "done") 52 | return nil 53 | }) 54 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 55 | handler(w, req, nil) 56 | })) 57 | defer ts.Close() 58 | req, err := http.NewRequest("GET", ts.URL, nil) 59 | c.Assert(err, qt.Equals, nil) 60 | client := httpbakery.NewClient() 61 | t0 := time.Now() 62 | resp, err := client.Do(req) 63 | c.Assert(err, qt.Equals, nil) 64 | c.Check(errorCalled, qt.Equals, 1) 65 | body, _ := ioutil.ReadAll(resp.Body) 66 | c.Assert(resp.StatusCode, qt.Equals, http.StatusOK, qt.Commentf("body: %q", body)) 67 | mss := httpbakery.MacaroonsForURL(client.Jar, mustParseURL(discharger.Location())) 68 | c.Assert(mss, qt.HasLen, 1) 69 | t1, ok := checkers.MacaroonsExpiryTime(b.Checker.Namespace(), mss[0]) 70 | c.Assert(ok, qt.Equals, true) 71 | want := t0.Add(expectedExpiry) 72 | if t1.Before(want) || t1.After(want.Add(time.Second)) { 73 | c.Fatalf("time out of range; got %v want %v", t1, want) 74 | } 75 | } 76 | 77 | func TestOvenWithAuthzMacaroon(t *testing.T) { 78 | c := qt.New(t) 79 | discharger := newTestIdentityServer() 80 | defer discharger.Close() 81 | discharger2 := bakerytest.NewDischarger(nil) 82 | defer discharger2.Close() 83 | 84 | locator := httpbakery.NewThirdPartyLocator(nil, nil) 85 | locator.AllowInsecure() 86 | 87 | key, err := bakery.GenerateKey() 88 | if err != nil { 89 | panic(err) 90 | } 91 | b := identchecker.NewBakery(identchecker.BakeryParams{ 92 | Location: "here", 93 | Locator: locator, 94 | Key: key, 95 | Checker: httpbakery.NewChecker(), 96 | IdentityClient: discharger, 97 | Authorizer: identchecker.AuthorizerFunc(func(ctx context.Context, id identchecker.Identity, op bakery.Op) (bool, []checkers.Caveat, error) { 98 | if id == nil { 99 | return false, nil, nil 100 | } 101 | return true, []checkers.Caveat{{ 102 | Location: discharger2.Location(), 103 | Condition: "something", 104 | }}, nil 105 | }), 106 | }) 107 | expectedAuthnExpiry := 5 * time.Minute 108 | expectedAuthzExpiry := time.Hour 109 | oven := &httpbakery.Oven{ 110 | Oven: b.Oven, 111 | AuthnExpiry: expectedAuthnExpiry, 112 | AuthzExpiry: expectedAuthzExpiry, 113 | } 114 | errorCalled := 0 115 | handler := httpReqServer.HandleErrors(func(p httprequest.Params) error { 116 | if _, err := b.Checker.Auth(httpbakery.RequestMacaroons(p.Request)...).Allow(p.Context, bakery.Op{"something", "read"}); err != nil { 117 | errorCalled++ 118 | return oven.Error(testContext, p.Request, err) 119 | } 120 | fmt.Fprintf(p.Response, "done") 121 | return nil 122 | }) 123 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 124 | handler(w, req, nil) 125 | })) 126 | defer ts.Close() 127 | req, err := http.NewRequest("GET", ts.URL, nil) 128 | c.Assert(err, qt.Equals, nil) 129 | client := httpbakery.NewClient() 130 | t0 := time.Now() 131 | resp, err := client.Do(req) 132 | c.Assert(err, qt.Equals, nil) 133 | c.Check(errorCalled, qt.Equals, 2) 134 | body, _ := ioutil.ReadAll(resp.Body) 135 | c.Assert(resp.StatusCode, qt.Equals, http.StatusOK, qt.Commentf("body: %q", body)) 136 | 137 | cookies := client.Jar.Cookies(mustParseURL(discharger.Location())) 138 | for i, cookie := range cookies { 139 | c.Logf("cookie %d: %s %q", i, cookie.Name, cookie.Value) 140 | } 141 | mss := httpbakery.MacaroonsForURL(client.Jar, mustParseURL(discharger.Location())) 142 | c.Assert(mss, qt.HasLen, 2) 143 | 144 | // The cookie jar returns otherwise-similar cookies in the order 145 | // they were added, so the authn macaroon will be first. 146 | t1, ok := checkers.MacaroonsExpiryTime(b.Checker.Namespace(), mss[0]) 147 | c.Assert(ok, qt.Equals, true) 148 | want := t0.Add(expectedAuthnExpiry) 149 | if t1.Before(want) || t1.After(want.Add(time.Second)) { 150 | c.Fatalf("time out of range; got %v want %v", t1, want) 151 | } 152 | 153 | t1, ok = checkers.MacaroonsExpiryTime(b.Checker.Namespace(), mss[1]) 154 | c.Assert(ok, qt.Equals, true) 155 | want = t0.Add(expectedAuthzExpiry) 156 | if t1.Before(want) || t1.After(want.Add(time.Second)) { 157 | c.Fatalf("time out of range; got %v want %v", t1, want) 158 | } 159 | } 160 | 161 | type testIdentityServer struct { 162 | *bakerytest.Discharger 163 | } 164 | 165 | func newTestIdentityServer() *testIdentityServer { 166 | checker := func(ctx context.Context, p httpbakery.ThirdPartyCaveatCheckerParams) ([]checkers.Caveat, error) { 167 | if string(p.Caveat.Condition) != "is-authenticated-user" { 168 | return nil, errgo.New("unexpected caveat") 169 | } 170 | return []checkers.Caveat{ 171 | checkers.DeclaredCaveat("username", "bob"), 172 | }, nil 173 | } 174 | discharger := bakerytest.NewDischarger(nil) 175 | discharger.CheckerP = httpbakery.ThirdPartyCaveatCheckerPFunc(checker) 176 | return &testIdentityServer{ 177 | Discharger: discharger, 178 | } 179 | } 180 | 181 | func (s *testIdentityServer) IdentityFromContext(ctx context.Context) (identchecker.Identity, []checkers.Caveat, error) { 182 | return nil, []checkers.Caveat{{ 183 | Location: s.Location(), 184 | Condition: "is-authenticated-user", 185 | }}, nil 186 | } 187 | 188 | func (s *testIdentityServer) DeclaredIdentity(ctx context.Context, declared map[string]string) (identchecker.Identity, error) { 189 | username, ok := declared["username"] 190 | if !ok { 191 | return nil, errgo.New("no username declared") 192 | } 193 | return identchecker.SimpleIdentity(username), nil 194 | } 195 | -------------------------------------------------------------------------------- /httpbakery/request.go: -------------------------------------------------------------------------------- 1 | package httpbakery 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "io" 7 | "net/http" 8 | "reflect" 9 | "sync" 10 | "sync/atomic" 11 | 12 | "gopkg.in/errgo.v1" 13 | ) 14 | 15 | // newRetrableRequest wraps an HTTP request so that it can 16 | // be retried without incurring race conditions and reports 17 | // whether the request can be retried. 18 | // The client instance will be used to make the request 19 | // when the do method is called. 20 | // 21 | // Because http.NewRequest often wraps its request bodies 22 | // with ioutil.NopCloser, which hides whether the body is 23 | // seekable, we extract the seeker from inside the nopCloser if 24 | // possible. 25 | // 26 | // We also work around Go issue 12796 by preventing concurrent 27 | // reads to the underlying reader after the request body has 28 | // been closed by Client.Do. 29 | // 30 | // The returned value should be closed after use. 31 | func newRetryableRequest(client *http.Client, req *http.Request) (*retryableRequest, bool) { 32 | if req.Body == nil { 33 | return &retryableRequest{ 34 | client: client, 35 | ref: 1, 36 | req: req, 37 | origCookie: req.Header.Get("Cookie"), 38 | }, true 39 | } 40 | body := seekerFromBody(req.Body) 41 | if body == nil { 42 | return nil, false 43 | } 44 | return &retryableRequest{ 45 | client: client, 46 | ref: 1, 47 | req: req, 48 | body: body, 49 | origCookie: req.Header.Get("Cookie"), 50 | }, true 51 | } 52 | 53 | type retryableRequest struct { 54 | client *http.Client 55 | ref int32 56 | origCookie string 57 | body readSeekCloser 58 | readStopper *readStopper 59 | req *http.Request 60 | } 61 | 62 | // do performs the HTTP request. 63 | func (rreq *retryableRequest) do(ctx context.Context) (*http.Response, error) { 64 | req, err := rreq.prepare() 65 | if err != nil { 66 | return nil, errgo.Mask(err) 67 | } 68 | return rreq.client.Do(req.WithContext(ctx)) 69 | } 70 | 71 | // prepare returns a new HTTP request object 72 | // by copying the original request and seeking 73 | // back to the start of the original body if needed. 74 | // 75 | // It needs to make a copy of the request because 76 | // the HTTP code can access the Request.Body field 77 | // after Client.Do has returned, which means we can't 78 | // replace it for the second request. 79 | func (rreq *retryableRequest) prepare() (*http.Request, error) { 80 | req := new(http.Request) 81 | *req = *rreq.req 82 | // Make sure that the original cookie header is still in place 83 | // so that we only end up with the cookies that are actually 84 | // added by the HTTP cookie logic, and not the ones that were 85 | // added in previous requests too. 86 | req.Header.Set("Cookie", rreq.origCookie) 87 | if rreq.body == nil { 88 | // No need for any of the seek shenanigans. 89 | return req, nil 90 | } 91 | if rreq.readStopper != nil { 92 | // We've made a previous request. Close its request 93 | // body so it can't interfere with the new request's body 94 | // and then seek back to the start. 95 | rreq.readStopper.Close() 96 | if _, err := rreq.body.Seek(0, 0); err != nil { 97 | return nil, errgo.Notef(err, "cannot seek to start of request body") 98 | } 99 | } 100 | atomic.AddInt32(&rreq.ref, 1) 101 | // Replace the request body with a new readStopper so that 102 | // we can stop a second request from interfering with current 103 | // request's body. 104 | rreq.readStopper = &readStopper{ 105 | rreq: rreq, 106 | r: rreq.body, 107 | } 108 | req.Body = rreq.readStopper 109 | return req, nil 110 | } 111 | 112 | // close closes the request. It closes the underlying reader 113 | // when all references have gone. 114 | func (req *retryableRequest) close() error { 115 | if atomic.AddInt32(&req.ref, -1) == 0 && req.body != nil { 116 | // We've closed it for the last time, so actually close 117 | // the original body. 118 | return req.body.Close() 119 | } 120 | return nil 121 | } 122 | 123 | // readStopper works around an issue with the net/http 124 | // package (see http://golang.org/issue/12796). 125 | // Because the first HTTP request might not have finished 126 | // reading from its body when it returns, we need to 127 | // ensure that the second request does not race on Read, 128 | // so this type implements a Reader that prevents all Read 129 | // calls to the underlying Reader after Close has been called. 130 | type readStopper struct { 131 | rreq *retryableRequest 132 | mu sync.Mutex 133 | r io.ReadSeeker 134 | } 135 | 136 | func (r *readStopper) Read(buf []byte) (int, error) { 137 | r.mu.Lock() 138 | defer r.mu.Unlock() 139 | if r.r == nil { 140 | // Note: we have to use io.EOF here because otherwise 141 | // another connection can in rare circumstances be 142 | // polluted by the error returned here. Although this 143 | // means the file may appear truncated to the server, 144 | // that shouldn't matter because the body will only 145 | // be closed after the server has replied. 146 | return 0, io.EOF 147 | } 148 | return r.r.Read(buf) 149 | } 150 | 151 | func (r *readStopper) Close() error { 152 | r.mu.Lock() 153 | alreadyClosed := r.r == nil 154 | r.r = nil 155 | r.mu.Unlock() 156 | if alreadyClosed { 157 | return nil 158 | } 159 | return r.rreq.close() 160 | } 161 | 162 | var nopCloserType = reflect.TypeOf(io.NopCloser(nil)) 163 | var nopCloserWriterToType = reflect.TypeOf(io.NopCloser(bytes.NewReader([]byte{}))) 164 | 165 | type readSeekCloser interface { 166 | io.ReadSeeker 167 | io.Closer 168 | } 169 | 170 | // seekerFromBody tries to obtain a seekable reader 171 | // from the given request body. 172 | func seekerFromBody(r io.ReadCloser) readSeekCloser { 173 | if r, ok := r.(readSeekCloser); ok { 174 | return r 175 | } 176 | rv := reflect.ValueOf(r) 177 | if rv.Type() != nopCloserType && rv.Type() != nopCloserWriterToType { 178 | return nil 179 | } 180 | // It's a value created by nopCloser. Extract the 181 | // underlying Reader. Note that this works 182 | // because the ioutil.nopCloser type exports 183 | // its Reader field. 184 | rs, ok := rv.Field(0).Interface().(io.ReadSeeker) 185 | if !ok { 186 | return nil 187 | } 188 | return readSeekerWithNopClose{rs} 189 | } 190 | 191 | type readSeekerWithNopClose struct { 192 | io.ReadSeeker 193 | } 194 | 195 | func (r readSeekerWithNopClose) Close() error { 196 | return nil 197 | } 198 | -------------------------------------------------------------------------------- /httpbakery/visitor.go: -------------------------------------------------------------------------------- 1 | package httpbakery 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "net/url" 7 | 8 | "gopkg.in/errgo.v1" 9 | "gopkg.in/httprequest.v1" 10 | 11 | "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" 12 | ) 13 | 14 | // TODO(rog) rename this file. 15 | 16 | // legacyGetInteractionMethods queries a URL as found in an 17 | // ErrInteractionRequired VisitURL field to find available interaction 18 | // methods. 19 | // 20 | // It does this by sending a GET request to the URL with the Accept 21 | // header set to "application/json" and parsing the resulting 22 | // response as a map[string]string. 23 | // 24 | // It uses the given Doer to execute the HTTP GET request. 25 | func legacyGetInteractionMethods(ctx context.Context, logger bakery.Logger, client httprequest.Doer, u *url.URL) map[string]*url.URL { 26 | methodURLs, err := legacyGetInteractionMethods1(ctx, client, u) 27 | if err != nil { 28 | // When a discharger doesn't support retrieving interaction methods, 29 | // we expect to get an error, because it's probably returning an HTML 30 | // page not JSON. 31 | if logger != nil { 32 | logger.Debugf(ctx, "ignoring error: cannot get interaction methods: %v; %s", err, errgo.Details(err)) 33 | } 34 | methodURLs = make(map[string]*url.URL) 35 | } 36 | if methodURLs["interactive"] == nil { 37 | // There's no "interactive" method returned, but we know 38 | // the server does actually support it, because all dischargers 39 | // are required to, so fill it in with the original URL. 40 | methodURLs["interactive"] = u 41 | } 42 | return methodURLs 43 | } 44 | 45 | func legacyGetInteractionMethods1(ctx context.Context, client httprequest.Doer, u *url.URL) (map[string]*url.URL, error) { 46 | httpReqClient := &httprequest.Client{ 47 | Doer: client, 48 | } 49 | req, err := http.NewRequest("GET", u.String(), nil) 50 | if err != nil { 51 | return nil, errgo.Notef(err, "cannot create request") 52 | } 53 | req.Header.Set("Accept", "application/json") 54 | var methodURLStrs map[string]string 55 | if err := httpReqClient.Do(ctx, req, &methodURLStrs); err != nil { 56 | return nil, errgo.Mask(err) 57 | } 58 | // Make all the URLs relative to the request URL. 59 | methodURLs := make(map[string]*url.URL) 60 | for m, urlStr := range methodURLStrs { 61 | relURL, err := url.Parse(urlStr) 62 | if err != nil { 63 | return nil, errgo.Notef(err, "invalid URL for interaction method %q", m) 64 | } 65 | methodURLs[m] = u.ResolveReference(relURL) 66 | } 67 | return methodURLs, nil 68 | } 69 | -------------------------------------------------------------------------------- /httpbakery/visitor_test.go: -------------------------------------------------------------------------------- 1 | package httpbakery_test 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | "net/http/httptest" 8 | "net/url" 9 | "testing" 10 | 11 | qt "github.com/frankban/quicktest" 12 | 13 | "github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery" 14 | ) 15 | 16 | func TestLegacyGetInteractionMethodsGetFailure(t *testing.T) { 17 | c := qt.New(t) 18 | srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 19 | w.WriteHeader(http.StatusTeapot) 20 | w.Write([]byte("failure")) 21 | })) 22 | defer srv.Close() 23 | 24 | methods := httpbakery.LegacyGetInteractionMethods(testContext, nopLogger{}, http.DefaultClient, mustParseURL(srv.URL)) 25 | // On error, it falls back to just the single default interactive method. 26 | c.Assert(methods, qt.DeepEquals, map[string]*url.URL{ 27 | "interactive": mustParseURL(srv.URL), 28 | }) 29 | } 30 | 31 | func TestLegacyGetInteractionMethodsSuccess(t *testing.T) { 32 | c := qt.New(t) 33 | srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 34 | w.Header().Set("Content-Type", "application/json") 35 | fmt.Fprint(w, `{"method": "http://somewhere/something"}`) 36 | })) 37 | defer srv.Close() 38 | 39 | methods := httpbakery.LegacyGetInteractionMethods(testContext, nopLogger{}, http.DefaultClient, mustParseURL(srv.URL)) 40 | c.Assert(methods, qt.DeepEquals, map[string]*url.URL{ 41 | "interactive": mustParseURL(srv.URL), 42 | "method": mustParseURL("http://somewhere/something"), 43 | }) 44 | } 45 | 46 | func TestLegacyGetInteractionMethodsInvalidURL(t *testing.T) { 47 | c := qt.New(t) 48 | srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 49 | w.Header().Set("Content-Type", "application/json") 50 | fmt.Fprint(w, `{"method": ":::"}`) 51 | })) 52 | defer srv.Close() 53 | 54 | methods := httpbakery.LegacyGetInteractionMethods(testContext, nopLogger{}, http.DefaultClient, mustParseURL(srv.URL)) 55 | 56 | // On error, it falls back to just the single default interactive method. 57 | c.Assert(methods, qt.DeepEquals, map[string]*url.URL{ 58 | "interactive": mustParseURL(srv.URL), 59 | }) 60 | } 61 | 62 | type nopLogger struct{} 63 | 64 | func (nopLogger) Debugf(context.Context, string, ...interface{}) {} 65 | func (nopLogger) Infof(context.Context, string, ...interface{}) {} 66 | -------------------------------------------------------------------------------- /internal/httputil/relativeurl.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Canonical Ltd. 2 | // Licensed under the LGPLv3, see LICENCE file for details. 3 | 4 | // Note: this code was copied from github.com/juju/utils. 5 | 6 | // Package httputil holds utility functions related to net/http. 7 | package httputil 8 | 9 | import ( 10 | "errors" 11 | "strings" 12 | ) 13 | 14 | // RelativeURLPath returns a relative URL path that is lexically 15 | // equivalent to targpath when interpreted by url.URL.ResolveReference. 16 | // On success, the returned path will always be non-empty and relative 17 | // to basePath, even if basePath and targPath share no elements. 18 | // 19 | // It is assumed that both basePath and targPath are normalized 20 | // (have no . or .. elements). 21 | // 22 | // An error is returned if basePath or targPath are not absolute paths. 23 | func RelativeURLPath(basePath, targPath string) (string, error) { 24 | if !strings.HasPrefix(basePath, "/") { 25 | return "", errors.New("non-absolute base URL") 26 | } 27 | if !strings.HasPrefix(targPath, "/") { 28 | return "", errors.New("non-absolute target URL") 29 | } 30 | baseParts := strings.Split(basePath, "/") 31 | targParts := strings.Split(targPath, "/") 32 | 33 | // For the purposes of dotdot, the last element of 34 | // the paths are irrelevant. We save the last part 35 | // of the target path for later. 36 | lastElem := targParts[len(targParts)-1] 37 | baseParts = baseParts[0 : len(baseParts)-1] 38 | targParts = targParts[0 : len(targParts)-1] 39 | 40 | // Find the common prefix between the two paths: 41 | var i int 42 | for ; i < len(baseParts); i++ { 43 | if i >= len(targParts) || baseParts[i] != targParts[i] { 44 | break 45 | } 46 | } 47 | dotdotCount := len(baseParts) - i 48 | targOnly := targParts[i:] 49 | result := make([]string, 0, dotdotCount+len(targOnly)+1) 50 | for i := 0; i < dotdotCount; i++ { 51 | result = append(result, "..") 52 | } 53 | result = append(result, targOnly...) 54 | result = append(result, lastElem) 55 | final := strings.Join(result, "/") 56 | if final == "" { 57 | // If the final result is empty, the last element must 58 | // have been empty, so the target was slash terminated 59 | // and there were no previous elements, so "." 60 | // is appropriate. 61 | final = "." 62 | } 63 | return final, nil 64 | } 65 | -------------------------------------------------------------------------------- /internal/httputil/relativeurl_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Canonical Ltd. 2 | // Licensed under the LGPLv3, see LICENCE file for details. 3 | 4 | // Note: this code was copied from github.com/juju/utils. 5 | 6 | package httputil_test 7 | 8 | import ( 9 | "net/url" 10 | "testing" 11 | 12 | qt "github.com/frankban/quicktest" 13 | 14 | "github.com/go-macaroon-bakery/macaroon-bakery/v3/internal/httputil" 15 | ) 16 | 17 | var relativeURLTests = []struct { 18 | base string 19 | target string 20 | expect string 21 | expectError string 22 | }{{ 23 | expectError: "non-absolute base URL", 24 | }, { 25 | base: "/foo", 26 | expectError: "non-absolute target URL", 27 | }, { 28 | base: "foo", 29 | expectError: "non-absolute base URL", 30 | }, { 31 | base: "/foo", 32 | target: "foo", 33 | expectError: "non-absolute target URL", 34 | }, { 35 | base: "/foo", 36 | target: "/bar", 37 | expect: "bar", 38 | }, { 39 | base: "/foo/", 40 | target: "/bar", 41 | expect: "../bar", 42 | }, { 43 | base: "/bar", 44 | target: "/foo/", 45 | expect: "foo/", 46 | }, { 47 | base: "/foo/", 48 | target: "/bar/", 49 | expect: "../bar/", 50 | }, { 51 | base: "/foo/bar", 52 | target: "/bar/", 53 | expect: "../bar/", 54 | }, { 55 | base: "/foo/bar/", 56 | target: "/bar/", 57 | expect: "../../bar/", 58 | }, { 59 | base: "/foo/bar/baz", 60 | target: "/foo/targ", 61 | expect: "../targ", 62 | }, { 63 | base: "/foo/bar/baz/frob", 64 | target: "/foo/bar/one/two/", 65 | expect: "../one/two/", 66 | }, { 67 | base: "/foo/bar/baz/", 68 | target: "/foo/targ", 69 | expect: "../../targ", 70 | }, { 71 | base: "/foo/bar/baz/frob/", 72 | target: "/foo/bar/one/two/", 73 | expect: "../../one/two/", 74 | }, { 75 | base: "/foo/bar", 76 | target: "/foot/bar", 77 | expect: "../foot/bar", 78 | }, { 79 | base: "/foo/bar/baz/frob", 80 | target: "/foo/bar", 81 | expect: "../../bar", 82 | }, { 83 | base: "/foo/bar/baz/frob/", 84 | target: "/foo/bar", 85 | expect: "../../../bar", 86 | }, { 87 | base: "/foo/bar/baz/frob/", 88 | target: "/foo/bar/", 89 | expect: "../../", 90 | }, { 91 | base: "/foo/bar/baz", 92 | target: "/foo/bar/other", 93 | expect: "other", 94 | }, { 95 | base: "/foo/bar/", 96 | target: "/foo/bar/", 97 | expect: ".", 98 | }, { 99 | base: "/foo/bar", 100 | target: "/foo/bar", 101 | expect: "bar", 102 | }, { 103 | base: "/foo/bar/", 104 | target: "/foo/bar/", 105 | expect: ".", 106 | }, { 107 | base: "/foo/bar", 108 | target: "/foo/", 109 | expect: ".", 110 | }, { 111 | base: "/foo", 112 | target: "/", 113 | expect: ".", 114 | }, { 115 | base: "/foo/", 116 | target: "/", 117 | expect: "../", 118 | }, { 119 | base: "/foo/bar", 120 | target: "/", 121 | expect: "../", 122 | }, { 123 | base: "/foo/bar/", 124 | target: "/", 125 | expect: "../../", 126 | }} 127 | 128 | func TestRelativeURL(t *testing.T) { 129 | c := qt.New(t) 130 | for i, test := range relativeURLTests { 131 | c.Logf("test %d: %q %q", i, test.base, test.target) 132 | // Sanity check the test itself. 133 | if test.expectError == "" { 134 | baseURL := &url.URL{Path: test.base} 135 | expectURL := &url.URL{Path: test.expect} 136 | targetURL := baseURL.ResolveReference(expectURL) 137 | c.Check(targetURL.Path, qt.Equals, test.target, qt.Commentf("resolve reference failure (%q + %q != %q)", test.base, test.expect, test.target)) 138 | } 139 | 140 | result, err := httputil.RelativeURLPath(test.base, test.target) 141 | if test.expectError != "" { 142 | c.Assert(err, qt.ErrorMatches, test.expectError) 143 | c.Assert(result, qt.Equals, "") 144 | } else { 145 | c.Assert(err, qt.IsNil) 146 | c.Check(result, qt.Equals, test.expect) 147 | } 148 | } 149 | } 150 | --------------------------------------------------------------------------------