github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/api/userpass_login_provider.go (about) 1 // Copyright 2024 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package api 5 6 import ( 7 "context" 8 "net/url" 9 "os" 10 "runtime/debug" 11 12 "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" 13 "github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery" 14 "github.com/juju/errors" 15 "github.com/juju/featureflag" 16 "github.com/juju/names/v5" 17 "github.com/juju/utils/v3" 18 "github.com/juju/version/v2" 19 "gopkg.in/macaroon.v2" 20 21 "github.com/juju/juju/api/base" 22 "github.com/juju/juju/feature" 23 "github.com/juju/juju/rpc" 24 "github.com/juju/juju/rpc/params" 25 jujuversion "github.com/juju/juju/version" 26 ) 27 28 // NewUserpassLoginProvider returns a LoginProvider implementation that 29 // authenticates the entity with the given name and password or macaroons. The nonce 30 // should be empty unless logging in as a machine agent. 31 func NewUserpassLoginProvider( 32 tag names.Tag, 33 password string, 34 nonce string, 35 macaroons []macaroon.Slice, 36 bakeryClient *httpbakery.Client, 37 cookieURL *url.URL, 38 ) *userpassLoginProvider { 39 return &userpassLoginProvider{ 40 tag: tag, 41 password: password, 42 nonce: nonce, 43 macaroons: macaroons, 44 bakeryClient: bakeryClient, 45 cookieURL: cookieURL, 46 } 47 } 48 49 // userpassLoginProvider provides the default juju login provider that 50 // authenticates the entity with the given name and password or macaroons. The 51 // nonce should be empty unless logging in as a machine agent. 52 type userpassLoginProvider struct { 53 tag names.Tag 54 password string 55 nonce string 56 macaroons []macaroon.Slice 57 bakeryClient *httpbakery.Client 58 cookieURL *url.URL 59 } 60 61 // Login implements the LoginProvider.Login method. 62 // 63 // It authenticates as the entity with the given name and password 64 // or macaroons. Subsequent requests on the state will act as that entity. 65 func (p *userpassLoginProvider) Login(ctx context.Context, caller base.APICaller) (*LoginResultParams, error) { 66 var result params.LoginResult 67 request := ¶ms.LoginRequest{ 68 AuthTag: tagToString(p.tag), 69 Credentials: p.password, 70 Nonce: p.nonce, 71 Macaroons: p.macaroons, 72 BakeryVersion: bakery.LatestVersion, 73 CLIArgs: utils.CommandString(os.Args...), 74 ClientVersion: jujuversion.Current.String(), 75 } 76 // If we are in developer mode, add the stack location as user data to the 77 // login request. This will allow the apiserver to connect connection ids 78 // to the particular place that initiated the connection. 79 if featureflag.Enabled(feature.DeveloperMode) { 80 request.UserData = string(debug.Stack()) 81 } 82 83 if p.password == "" { 84 // Add any macaroons from the cookie jar that might work for 85 // authenticating the login request. 86 request.Macaroons = append(request.Macaroons, 87 httpbakery.MacaroonsForURL(p.bakeryClient.Jar, p.cookieURL)..., 88 ) 89 } 90 err := caller.APICall("Admin", 3, "", "Login", request, &result) 91 if err != nil { 92 if !params.IsRedirect(err) { 93 return nil, errors.Trace(err) 94 } 95 96 if rpcErr, ok := errors.Cause(err).(*rpc.RequestError); ok { 97 var redirInfo params.RedirectErrorInfo 98 err := rpcErr.UnmarshalInfo(&redirInfo) 99 if err == nil && redirInfo.CACert != "" && len(redirInfo.Servers) != 0 { 100 var controllerTag names.ControllerTag 101 if redirInfo.ControllerTag != "" { 102 if controllerTag, err = names.ParseControllerTag(redirInfo.ControllerTag); err != nil { 103 return nil, errors.Trace(err) 104 } 105 } 106 107 return nil, &RedirectError{ 108 Servers: params.ToMachineHostsPorts(redirInfo.Servers), 109 CACert: redirInfo.CACert, 110 ControllerTag: controllerTag, 111 ControllerAlias: redirInfo.ControllerAlias, 112 FollowRedirect: false, // user-action required 113 } 114 } 115 } 116 117 // We've been asked to redirect. Find out the redirection info. 118 // If the rpc packet allowed us to return arbitrary information in 119 // an error, we'd probably put this information in the Login response, 120 // but we can't do that currently. 121 var resp params.RedirectInfoResult 122 if err := caller.APICall("Admin", 3, "", "RedirectInfo", nil, &resp); err != nil { 123 return nil, errors.Annotatef(err, "cannot get redirect addresses") 124 } 125 return nil, &RedirectError{ 126 Servers: params.ToMachineHostsPorts(resp.Servers), 127 CACert: resp.CACert, 128 FollowRedirect: true, // JAAS-type redirect 129 } 130 } 131 if result.DischargeRequired != nil || result.BakeryDischargeRequired != nil { 132 // The result contains a discharge-required 133 // macaroon. We discharge it and retry 134 // the login request with the original macaroon 135 // and its discharges. 136 if result.DischargeRequiredReason == "" { 137 result.DischargeRequiredReason = "no reason given for discharge requirement" 138 } 139 // Prefer the newer bakery.v2 macaroon. 140 dcMac := result.BakeryDischargeRequired 141 if dcMac == nil { 142 dcMac, err = bakery.NewLegacyMacaroon(result.DischargeRequired) 143 if err != nil { 144 return nil, errors.Trace(err) 145 } 146 } 147 if err := p.bakeryClient.HandleError(ctx, p.cookieURL, &httpbakery.Error{ 148 Message: result.DischargeRequiredReason, 149 Code: httpbakery.ErrDischargeRequired, 150 Info: &httpbakery.ErrorInfo{ 151 Macaroon: dcMac, 152 MacaroonPath: "/", 153 }, 154 }); err != nil { 155 cause := errors.Cause(err) 156 if httpbakery.IsInteractionError(cause) { 157 // Just inform the user of the reason for the 158 // failure, e.g. because the username/password 159 // they presented was invalid. 160 err = cause.(*httpbakery.InteractionError).Reason 161 } 162 return nil, errors.Trace(err) 163 } 164 // Add the macaroons that have been saved by HandleError to our login request. 165 request.Macaroons = httpbakery.MacaroonsForURL(p.bakeryClient.Jar, p.cookieURL) 166 result = params.LoginResult{} // zero result 167 err = caller.APICall("Admin", 3, "", "Login", request, &result) 168 if err != nil { 169 return nil, errors.Trace(err) 170 } 171 if result.DischargeRequired != nil { 172 return nil, errors.Errorf("login with discharged macaroons failed: %s", result.DischargeRequiredReason) 173 } 174 } 175 176 var controllerAccess string 177 var modelAccess string 178 tag := p.tag 179 if result.UserInfo != nil { 180 tag, err = names.ParseTag(result.UserInfo.Identity) 181 if err != nil { 182 return nil, errors.Trace(err) 183 } 184 controllerAccess = result.UserInfo.ControllerAccess 185 modelAccess = result.UserInfo.ModelAccess 186 } 187 servers := params.ToMachineHostsPorts(result.Servers) 188 serverVersion, err := version.Parse(result.ServerVersion) 189 if err != nil { 190 return nil, errors.Trace(err) 191 } 192 return &LoginResultParams{ 193 tag: tag, 194 modelTag: result.ModelTag, 195 controllerTag: result.ControllerTag, 196 servers: servers, 197 publicDNSName: result.PublicDNSName, 198 facades: result.Facades, 199 modelAccess: modelAccess, 200 controllerAccess: controllerAccess, 201 serverVersion: serverVersion, 202 }, nil 203 }