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 := &params.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  }