github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/api/authentication/interactor.go (about)

     1  // Copyright 2016 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package authentication
     5  
     6  import (
     7  	"context"
     8  	"encoding/json"
     9  	"net/http"
    10  	"net/url"
    11  
    12  	"github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery"
    13  	"github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery/form"
    14  	"github.com/juju/errors"
    15  	"gopkg.in/httprequest.v1"
    16  )
    17  
    18  const authMethod = "juju_userpass"
    19  
    20  // Interactor is a httpbakery.Interactor that will login directly
    21  // to the Juju controller using password authentication. This
    22  // only applies when logging in as a local user.
    23  type Interactor struct {
    24  	username    string
    25  	getPassword func(string) (string, error)
    26  }
    27  
    28  // NewInteractor returns a new Interactor.
    29  func NewInteractor(username string, getPassword func(string) (string, error)) httpbakery.Interactor {
    30  	return &Interactor{
    31  		username:    username,
    32  		getPassword: getPassword,
    33  	}
    34  }
    35  
    36  // Kind implements httpbakery.Interactor.Kind.
    37  func (i Interactor) Kind() string {
    38  	return authMethod
    39  }
    40  
    41  // Interact implements httpbakery.Interactor for the Interactor.
    42  func (i Interactor) Interact(ctx context.Context, client *httpbakery.Client, location string, interactionRequiredErr *httpbakery.Error) (*httpbakery.DischargeToken, error) {
    43  	var p form.InteractionInfo
    44  	if err := interactionRequiredErr.InteractionMethod(authMethod, &p); err != nil {
    45  		return nil, errors.Trace(err)
    46  	}
    47  	if p.URL == "" {
    48  		return nil, errors.New("no URL found in form information")
    49  	}
    50  	schemaURL, err := relativeURL(location, p.URL)
    51  	if err != nil {
    52  		return nil, errors.Annotatef(err, "invalid url %q", p.URL)
    53  	}
    54  	httpReqClient := &httprequest.Client{
    55  		Doer: client,
    56  	}
    57  	password, err := i.getPassword(i.username)
    58  	if err != nil {
    59  		return nil, errors.Trace(err)
    60  	}
    61  	lr := form.LoginRequest{
    62  		Body: form.LoginBody{
    63  			Form: map[string]interface{}{
    64  				"user":     i.username,
    65  				"password": password,
    66  			},
    67  		},
    68  	}
    69  	var lresp form.LoginResponse
    70  	if err := httpReqClient.CallURL(ctx, schemaURL.String(), &lr, &lresp); err != nil {
    71  		return nil, errors.Annotate(err, "cannot submit form")
    72  	}
    73  	if lresp.Token == nil {
    74  		return nil, errors.New("no token found in form response")
    75  	}
    76  	return lresp.Token, nil
    77  }
    78  
    79  // relativeURL returns newPath relative to an original URL.
    80  func relativeURL(base, new string) (*url.URL, error) {
    81  	if new == "" {
    82  		return nil, errors.New("empty URL")
    83  	}
    84  	baseURL, err := url.Parse(base)
    85  	if err != nil {
    86  		return nil, errors.Annotate(err, "cannot parse URL")
    87  	}
    88  	newURL, err := url.Parse(new)
    89  	if err != nil {
    90  		return nil, errors.Annotate(err, "cannot parse URL")
    91  	}
    92  	return baseURL.ResolveReference(newURL), nil
    93  }
    94  
    95  // LegacyInteract implements httpbakery.LegacyInteractor for the Interactor.
    96  func (i *Interactor) LegacyInteract(ctx context.Context, client *httpbakery.Client, location string, methodURL *url.URL) error {
    97  	password, err := i.getPassword(i.username)
    98  	if err != nil {
    99  		return err
   100  	}
   101  
   102  	// POST to the URL with username and password.
   103  	resp, err := client.PostForm(methodURL.String(), url.Values{
   104  		"user":     {i.username},
   105  		"password": {password},
   106  	})
   107  	if err != nil {
   108  		return err
   109  	}
   110  	defer resp.Body.Close()
   111  
   112  	if resp.StatusCode == http.StatusOK {
   113  		return nil
   114  	}
   115  	var jsonError httpbakery.Error
   116  	if err := json.NewDecoder(resp.Body).Decode(&jsonError); err != nil {
   117  		return errors.Annotate(err, "unmarshalling error")
   118  	}
   119  	return &jsonError
   120  }
   121  
   122  // NewNotSupportedInteractor returns an interactor that does
   123  // not support any discharge workflow.
   124  func NewNotSupportedInteractor() httpbakery.Interactor {
   125  	return &notSupportedInteractor{}
   126  }
   127  
   128  type notSupportedInteractor struct {
   129  }
   130  
   131  // Kind implements httpbakery.Interactor for the Interactor.
   132  func (i notSupportedInteractor) Kind() string {
   133  	return authMethod
   134  }
   135  
   136  // Interact implements httpbakery.Interactor for the Interactor.
   137  func (i notSupportedInteractor) Interact(_ context.Context, _ *httpbakery.Client, location string, _ *httpbakery.Error) (*httpbakery.DischargeToken, error) {
   138  	return nil, errors.NotSupportedf("interaction for %s", location)
   139  }