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 }