Skip to content

Commit

Permalink
API client changes to use macaroon v2 and bakery client v2
Browse files Browse the repository at this point in the history
  • Loading branch information
wallyworld committed Dec 17, 2019
1 parent c1ca956 commit 9568a31
Show file tree
Hide file tree
Showing 39 changed files with 327 additions and 234 deletions.
18 changes: 13 additions & 5 deletions api/apiclient.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,13 @@ import (
"github.com/juju/utils/parallel"
"github.com/juju/version"
"gopkg.in/juju/names.v3"
"gopkg.in/macaroon-bakery.v2-unstable/httpbakery"
"gopkg.in/macaroon.v2-unstable"
"gopkg.in/macaroon-bakery.v2/httpbakery"
"gopkg.in/macaroon.v2"
"gopkg.in/retry.v1"

"github.com/juju/juju/api/base"
"github.com/juju/juju/apiserver/params"
"github.com/juju/juju/charmstore"
"github.com/juju/juju/core/network"
"github.com/juju/juju/rpc"
"github.com/juju/juju/rpc/jsoncodec"
Expand Down Expand Up @@ -66,6 +67,7 @@ type rpcConnection interface {

// state is the internal implementation of the Connection interface.
type state struct {
ctx context.Context
client rpcConnection
conn jsoncodec.JSONConn
clock clock.Clock
Expand Down Expand Up @@ -231,6 +233,7 @@ func Open(info *Info, opts DialOpts) (Connection, error) {
}

st := &state{
ctx: context.Background(),
client: client,
conn: dialResult.conn,
clock: opts.Clock,
Expand Down Expand Up @@ -315,6 +318,11 @@ func (t *hostSwitchingTransport) RoundTrip(req *http.Request) (*http.Response, e
return t.fallback.RoundTrip(req)
}

// Context returns the context associated with this state.
func (st *state) Context() context.Context {
return st.ctx
}

// ConnectStream implements StreamConnector.ConnectStream. The stream
// returned will apply a 30-second write deadline, so WriteJSON should
// only be called from one goroutine.
Expand Down Expand Up @@ -366,7 +374,7 @@ func (st *state) connectStreamWithRetry(path string, attrs url.Values, headers h
if params.ErrCode(err) != params.CodeDischargeRequired {
return nil, errors.Trace(err)
}
if err := st.bakeryClient.HandleError(st.cookieURL, bakeryError(err)); err != nil {
if err := st.bakeryClient.HandleError(st.ctx, st.cookieURL, bakeryError(err)); err != nil {
return nil, errors.Trace(err)
}
// Try again with the discharged macaroon.
Expand Down Expand Up @@ -484,7 +492,7 @@ func (st *state) addCookiesToHeader(h http.Header) error {
// logtransfer connection for a migration.)
// See https://bugs.launchpad.net/juju/+bug/1650451
for _, macaroon := range st.macaroons {
cookie, err := httpbakery.NewCookie(macaroon)
cookie, err := httpbakery.NewCookie(charmstore.MacaroonNamespace, macaroon)
if err != nil {
return errors.Trace(err)
}
Expand Down Expand Up @@ -1205,7 +1213,7 @@ func (s *state) Close() error {
}

// BakeryClient implements api.Connection.
func (s *state) BakeryClient() *httpbakery.Client {
func (s *state) BakeryClient() base.MacaroonDischarger {
return s.bakeryClient
}

Expand Down
32 changes: 19 additions & 13 deletions api/authentication/visitor.go → api/authentication/interactor.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,47 +4,53 @@
package authentication

import (
"context"
"encoding/json"
"net/http"
"net/url"

"github.com/juju/errors"
"gopkg.in/macaroon-bakery.v2-unstable/httpbakery"
schemaform "gopkg.in/juju/environschema.v1/form"
"gopkg.in/macaroon-bakery.v2/httpbakery"
"gopkg.in/macaroon-bakery.v2/httpbakery/form"
)

const authMethod = "juju_userpass"

// Visitor is a httpbakery.Visitor that will login directly
// to the Juju controller using password authentication. This
// only applies when logging in as a local user.
type Visitor struct {
type Interactor struct {
form.Interactor
username string
getPassword func(string) (string, error)
}

// NewVisitor returns a new Visitor.
func NewVisitor(username string, getPassword func(string) (string, error)) *Visitor {
return &Visitor{
// NewInteractor returns a new Interactor.
func NewInteractor(username string, getPassword func(string) (string, error)) httpbakery.Interactor {
return &Interactor{
Interactor: form.Interactor{Filler: schemaform.IOFiller{}},
username: username,
getPassword: getPassword,
}
}

// VisitWebPage is part of the httpbakery.Visitor interface.
func (v *Visitor) VisitWebPage(client *httpbakery.Client, methodURLs map[string]*url.URL) error {
methodURL := methodURLs[authMethod]
if methodURL == nil {
return httpbakery.ErrMethodNotSupported
}
// Kind implements httpbakery.Interactor.Kind.
func (i Interactor) Kind() string {
return authMethod
}

password, err := v.getPassword(v.username)
// LegacyInteract implements httpbakery.LegacyInteractor
// for the Interactor.
func (i *Interactor) LegacyInteract(ctx context.Context, client *httpbakery.Client, location string, methodURL *url.URL) error {
password, err := i.getPassword(i.username)
if err != nil {
return err
}

// POST to the URL with username and password.
resp, err := client.PostForm(methodURL.String(), url.Values{
"user": {v.username},
"user": {i.username},
"password": {password},
})
if err != nil {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
package authentication_test

import (
"context"
"net/http"
"net/http/cookiejar"
"net/http/httptest"
Expand All @@ -12,12 +13,12 @@ import (
"github.com/juju/testing"
jc "github.com/juju/testing/checkers"
gc "gopkg.in/check.v1"
"gopkg.in/macaroon-bakery.v2-unstable/httpbakery"
"gopkg.in/macaroon-bakery.v2/httpbakery"

"github.com/juju/juju/api/authentication"
)

type VisitorSuite struct {
type InteractorSuite struct {
testing.IsolationSuite

jar *cookiejar.Jar
Expand All @@ -26,9 +27,9 @@ type VisitorSuite struct {
handler http.Handler
}

var _ = gc.Suite(&VisitorSuite{})
var _ = gc.Suite(&InteractorSuite{})

func (s *VisitorSuite) SetUpTest(c *gc.C) {
func (s *InteractorSuite) SetUpTest(c *gc.C) {
s.IsolationSuite.SetUpTest(c)
var err error
s.jar, err = cookiejar.New(nil)
Expand All @@ -42,41 +43,40 @@ func (s *VisitorSuite) SetUpTest(c *gc.C) {
s.AddCleanup(func(c *gc.C) { s.server.Close() })
}

func (s *VisitorSuite) TestVisitWebPage(c *gc.C) {
v := authentication.NewVisitor("bob", func(username string) (string, error) {
func (s *InteractorSuite) TestInteract(c *gc.C) {
v := authentication.NewInteractor("bob", func(username string) (string, error) {
c.Assert(username, gc.Equals, "bob")
return "hunter2", nil
})
lv, ok := v.(httpbakery.LegacyInteractor)
c.Assert(ok, jc.IsTrue)
var formUser, formPassword string
s.handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
formUser = r.Form.Get("user")
formPassword = r.Form.Get("password")
})
err := v.VisitWebPage(s.client, map[string]*url.URL{
"juju_userpass": mustParseURL(s.server.URL),
})
err := lv.LegacyInteract(context.Background(), s.client, "", mustParseURL(s.server.URL))
c.Assert(err, jc.ErrorIsNil)
c.Assert(formUser, gc.Equals, "bob")
c.Assert(formPassword, gc.Equals, "hunter2")
}

func (s *VisitorSuite) TestVisitWebPageMethodNotSupported(c *gc.C) {
v := authentication.NewVisitor("bob", nil)
err := v.VisitWebPage(s.client, map[string]*url.URL{})
c.Assert(err, gc.Equals, httpbakery.ErrMethodNotSupported)
func (s *InteractorSuite) TestKind(c *gc.C) {
v := authentication.NewInteractor("bob", nil)
c.Assert(v.Kind(), gc.Equals, "juju_userpass")
}

func (s *VisitorSuite) TestVisitWebPageErrorResult(c *gc.C) {
v := authentication.NewVisitor("bob", func(username string) (string, error) {
func (s *InteractorSuite) TestInteractErrorResult(c *gc.C) {
v := authentication.NewInteractor("bob", func(username string) (string, error) {
return "hunter2", nil
})
lv, ok := v.(httpbakery.LegacyInteractor)
c.Assert(ok, jc.IsTrue)
s.handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Error(w, `{"Message":"bleh"}`, http.StatusInternalServerError)
})
err := v.VisitWebPage(s.client, map[string]*url.URL{
"juju_userpass": mustParseURL(s.server.URL),
})
err := lv.LegacyInteract(context.Background(), s.client, "", mustParseURL(s.server.URL))
c.Assert(err, gc.ErrorMatches, "bleh")
}

Expand Down
2 changes: 1 addition & 1 deletion api/backups/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ package backups

import (
"github.com/juju/errors"
"github.com/juju/httprequest"
"github.com/juju/loggo"
"gopkg.in/httprequest.v1"

"github.com/juju/juju/api/base"
)
Expand Down
3 changes: 2 additions & 1 deletion api/backups/download.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (
"net/http"

"github.com/juju/errors"
"github.com/juju/httprequest"
"gopkg.in/httprequest.v1"

"github.com/juju/juju/apiserver/params"
)
Expand All @@ -23,6 +23,7 @@ func (c *Client) Download(id string) (io.ReadCloser, error) {
// Send the request.
var resp *http.Response
err := c.client.Call(
c.facade.RawAPICaller().Context(),
&downloadParams{
Body: params.BackupsDownloadArgs{
ID: id,
Expand Down
10 changes: 5 additions & 5 deletions api/backups/upload.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,17 @@ func (c *Client) Upload(archive io.ReadSeeker, meta params.BackupsMetadataResult
meta.ID = ""
meta.Stored = time.Time{}

req, err := http.NewRequest("PUT", "/backups", nil)
if err != nil {
return "", errors.Trace(err)
}
body, contentType, err := httpattachment.NewBody(archive, meta, "juju-backup.tar.gz")
if err != nil {
return "", errors.Annotatef(err, "cannot create multipart body")
}
req, err := http.NewRequest("PUT", "/backups", body)
if err != nil {
return "", errors.Trace(err)
}
req.Header.Set("Content-Type", contentType)
var result params.BackupsUploadResult
if err := c.client.Do(req, body, &result); err != nil {
if err := c.client.Do(c.facade.RawAPICaller().Context(), req, &result); err != nil {
return "", errors.Trace(err)
}
return result.ID, nil
Expand Down
19 changes: 16 additions & 3 deletions api/base/caller.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@
package base

import (
"context"
"io"
"net/http"
"net/url"

"github.com/juju/httprequest"
"gopkg.in/httprequest.v1"
"gopkg.in/juju/names.v3"
"gopkg.in/macaroon-bakery.v2-unstable/httpbakery"
"gopkg.in/macaroon-bakery.v2/bakery"
"gopkg.in/macaroon.v2"
)

//go:generate mockgen -package mocks -destination mocks/caller_mock.go github.com/juju/juju/api/base APICaller,FacadeCaller
Expand Down Expand Up @@ -43,12 +45,23 @@ type APICaller interface {
HTTPClient() (*httprequest.Client, error)

// BakeryClient returns the bakery client for this connection.
BakeryClient() *httpbakery.Client
BakeryClient() MacaroonDischarger

// Context returns the standard context for this connection.
Context() context.Context

StreamConnector
ControllerStreamConnector
}

// MacaroonDischarger instances provide a method to discharge macaroons.
type MacaroonDischarger interface {
// DischargeAll attempts to acquire discharge macaroons for all the
// third party caveats in m, and returns a slice containing all
// of them bound to m.
DischargeAll(ctx context.Context, m *bakery.Macaroon) (macaroon.Slice, error)
}

// StreamConnector is implemented by the client-facing State object.
type StreamConnector interface {
// ConnectStream connects to the given HTTP websocket
Expand Down
24 changes: 18 additions & 6 deletions api/base/mocks/caller_mock.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 9568a31

Please sign in to comment.