github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/cmd/modelcmd/apicontext.go (about)

     1  // Copyright 2016 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package modelcmd
     5  
     6  import (
     7  	"net/http"
     8  	"net/url"
     9  	"os"
    10  
    11  	"github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery"
    12  	"github.com/juju/cmd/v3"
    13  	"github.com/juju/errors"
    14  	"github.com/juju/gnuflag"
    15  	"github.com/juju/idmclient/v2/ussologin"
    16  	"gopkg.in/juju/environschema.v1/form"
    17  
    18  	"github.com/juju/juju/api/authentication"
    19  	"github.com/juju/juju/jujuclient"
    20  )
    21  
    22  // apiContext holds the context required for making connections to
    23  // APIs used by juju.
    24  type apiContext struct {
    25  	// jar holds the internal version of the cookie jar - it has
    26  	// methods that clients should not use, such as Save.
    27  	jar        *domainCookieJar
    28  	interactor httpbakery.Interactor
    29  }
    30  
    31  // AuthOpts holds flags relating to authentication.
    32  type AuthOpts struct {
    33  	// NoBrowser specifies that web-browser-based auth should
    34  	// not be used when authenticating.
    35  	NoBrowser bool
    36  	// Embedded is true for commands run inside a controller.
    37  	Embedded bool
    38  }
    39  
    40  func (o *AuthOpts) SetFlags(f *gnuflag.FlagSet) {
    41  	f.BoolVar(&o.NoBrowser, "B", false, "Do not use web browser for authentication")
    42  	f.BoolVar(&o.NoBrowser, "no-browser-login", false, "")
    43  }
    44  
    45  // newAPIContext returns an API context that will use the given
    46  // context for user interactions when authorizing.
    47  // The returned API context must be closed after use.
    48  //
    49  // If ctxt is nil, no command-line authorization
    50  // will be supported.
    51  //
    52  // This function is provided for use by commands that cannot use
    53  // CommandBase. Most clients should use that instead.
    54  func newAPIContext(ctxt *cmd.Context, opts *AuthOpts, store jujuclient.CookieStore, controllerName string) (*apiContext, error) {
    55  	jar0, err := store.CookieJar(controllerName)
    56  	if err != nil {
    57  		return nil, errors.Trace(err)
    58  	}
    59  	// The JUJU_USER_DOMAIN environment variable specifies
    60  	// the preferred user domain when discharging third party caveats.
    61  	// We set up a cookie jar that will send it to all sites because
    62  	// we don't know where the third party might be.
    63  	jar := &domainCookieJar{
    64  		CookieJar: jar0,
    65  		domain:    os.Getenv("JUJU_USER_DOMAIN"),
    66  	}
    67  	var interactor httpbakery.Interactor
    68  	embedded := ctxt != nil && opts != nil && opts.Embedded
    69  	if embedded {
    70  		// Embedded commands don't yet support macaroon discharge workflow.
    71  		interactor = authentication.NewNotSupportedInteractor()
    72  	} else {
    73  		// Only support discharge interactions if command is not embedded.
    74  		noBrowser := ctxt != nil && opts != nil && opts.NoBrowser
    75  		if noBrowser {
    76  			filler := &form.IOFiller{
    77  				In:  ctxt.Stdin,
    78  				Out: ctxt.Stdout,
    79  			}
    80  			interactor = ussologin.NewInteractor(ussologin.StoreTokenGetter{
    81  				Store: jujuclient.NewTokenStore(),
    82  				TokenGetter: ussologin.FormTokenGetter{
    83  					Filler: filler,
    84  					Name:   "juju",
    85  				},
    86  			})
    87  		} else {
    88  			interactor = httpbakery.WebBrowserInteractor{}
    89  		}
    90  	}
    91  	return &apiContext{
    92  		jar:        jar,
    93  		interactor: interactor,
    94  	}, nil
    95  }
    96  
    97  // CookieJar returns the cookie jar used to make
    98  // HTTP requests.
    99  func (ctx *apiContext) CookieJar() http.CookieJar {
   100  	return ctx.jar
   101  }
   102  
   103  // NewBakeryClient returns a new httpbakery.Client, using the API context's
   104  // persistent cookie jar and web page visitor.
   105  func (ctx *apiContext) NewBakeryClient() *httpbakery.Client {
   106  	client := httpbakery.NewClient()
   107  	client.Jar = ctx.jar
   108  	if ctx.interactor != nil {
   109  		client.AddInteractor(ctx.interactor)
   110  	}
   111  	return client
   112  }
   113  
   114  // Close closes the API context, saving any cookies to the
   115  // persistent cookie jar.
   116  func (ctxt *apiContext) Close() error {
   117  	if err := ctxt.jar.Save(); err != nil {
   118  		return errors.Annotatef(err, "cannot save cookie jar")
   119  	}
   120  	return nil
   121  }
   122  
   123  const domainCookieName = "domain"
   124  
   125  // domainCookieJar implements a variant of CookieJar that
   126  // always includes a domain cookie regardless of the site.
   127  type domainCookieJar struct {
   128  	jujuclient.CookieJar
   129  	// domain holds the value of the domain cookie.
   130  	domain string
   131  }
   132  
   133  // Cookies implements http.CookieJar.Cookies by
   134  // adding the domain cookie when the domain is non-empty.
   135  func (j *domainCookieJar) Cookies(u *url.URL) []*http.Cookie {
   136  	cookies := j.CookieJar.Cookies(u)
   137  	if j.domain == "" {
   138  		return cookies
   139  	}
   140  	// Allow the site to override if it wants to.
   141  	for _, c := range cookies {
   142  		if c.Name == domainCookieName {
   143  			return cookies
   144  		}
   145  	}
   146  	return append(cookies, &http.Cookie{
   147  		Name:  domainCookieName,
   148  		Value: j.domain,
   149  	})
   150  }