github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/apiserver/stateauthenticator/auth.go (about)

     1  // Copyright 2018 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package stateauthenticator
     5  
     6  import (
     7  	"encoding/base64"
     8  	"net/http"
     9  	"strings"
    10  	"time"
    11  
    12  	"github.com/juju/clock"
    13  	"github.com/juju/errors"
    14  	"gopkg.in/juju/names.v2"
    15  	"gopkg.in/macaroon-bakery.v2-unstable/httpbakery"
    16  	"gopkg.in/macaroon.v2-unstable"
    17  
    18  	"github.com/juju/juju/apiserver/apiserverhttp"
    19  	"github.com/juju/juju/apiserver/authentication"
    20  	"github.com/juju/juju/apiserver/common"
    21  	"github.com/juju/juju/apiserver/httpcontext"
    22  	"github.com/juju/juju/apiserver/params"
    23  	"github.com/juju/juju/state"
    24  )
    25  
    26  // Authenticator is an implementation of httpcontext.Authenticator,
    27  // using *state.State for authentication.
    28  //
    29  // This Authenticator only works with requests that have been handled
    30  // by one of the httpcontext.*ModelHandler handlers.
    31  type Authenticator struct {
    32  	statePool   *state.StatePool
    33  	authContext *authContext
    34  }
    35  
    36  // NewAuthenticator returns a new Authenticator using the given StatePool.
    37  func NewAuthenticator(statePool *state.StatePool, clock clock.Clock) (*Authenticator, error) {
    38  	authContext, err := newAuthContext(statePool.SystemState(), clock)
    39  	if err != nil {
    40  		return nil, errors.Trace(err)
    41  	}
    42  	return &Authenticator{
    43  		statePool:   statePool,
    44  		authContext: authContext,
    45  	}, nil
    46  }
    47  
    48  // Maintain periodically expires local login interactions.
    49  func (a *Authenticator) Maintain(done <-chan struct{}) {
    50  	for {
    51  		select {
    52  		case <-done:
    53  			return
    54  		case <-a.authContext.clock.After(authentication.LocalLoginInteractionTimeout):
    55  			now := a.authContext.clock.Now()
    56  			a.authContext.localUserInteractions.Expire(now)
    57  		}
    58  	}
    59  }
    60  
    61  // CreateLocalLoginMacaroon is part of the
    62  // httpcontext.LocalMacaroonAuthenticator interface.
    63  func (a *Authenticator) CreateLocalLoginMacaroon(tag names.UserTag) (*macaroon.Macaroon, error) {
    64  	return a.authContext.CreateLocalLoginMacaroon(tag)
    65  }
    66  
    67  // AddHandlers adds the handlers to the given mux for handling local
    68  // macaroon logins.
    69  func (a *Authenticator) AddHandlers(mux *apiserverhttp.Mux) {
    70  	h := &localLoginHandlers{
    71  		authCtxt: a.authContext,
    72  		finder:   a.statePool.SystemState(),
    73  	}
    74  	h.AddHandlers(mux)
    75  }
    76  
    77  // Authenticate is part of the httpcontext.Authenticator interface.
    78  func (a *Authenticator) Authenticate(req *http.Request) (httpcontext.AuthInfo, error) {
    79  	modelUUID := httpcontext.RequestModelUUID(req)
    80  	if modelUUID == "" {
    81  		return httpcontext.AuthInfo{}, errors.New("model UUID not found")
    82  	}
    83  	loginRequest, err := LoginRequest(req)
    84  	if err != nil {
    85  		return httpcontext.AuthInfo{}, errors.Trace(err)
    86  	}
    87  	return a.AuthenticateLoginRequest(req.Host, modelUUID, loginRequest)
    88  }
    89  
    90  // AuthenticateLoginRequest authenticates a LoginRequest.
    91  //
    92  // TODO(axw) we shouldn't be using params types here.
    93  func (a *Authenticator) AuthenticateLoginRequest(
    94  	serverHost string,
    95  	modelUUID string,
    96  	req params.LoginRequest,
    97  ) (httpcontext.AuthInfo, error) {
    98  	var authTag names.Tag
    99  	if req.AuthTag != "" {
   100  		tag, err := names.ParseTag(req.AuthTag)
   101  		if err != nil {
   102  			return httpcontext.AuthInfo{}, errors.Trace(err)
   103  		}
   104  		authTag = tag
   105  	}
   106  
   107  	st, err := a.statePool.Get(modelUUID)
   108  	if err != nil {
   109  		return httpcontext.AuthInfo{}, errors.Trace(err)
   110  	}
   111  	defer st.Release()
   112  
   113  	authenticator := a.authContext.authenticator(serverHost)
   114  	authInfo, err := a.checkCreds(st.State, req, authTag, true, authenticator)
   115  	if err != nil {
   116  		if common.IsDischargeRequiredError(err) || errors.IsNotProvisioned(err) {
   117  			// TODO(axw) move out of common?
   118  			return httpcontext.AuthInfo{}, errors.Trace(err)
   119  		}
   120  		if _, ok := authTag.(names.MachineTag); ok && !st.IsController() {
   121  			// Controller agents are allowed to log into any model.
   122  			var err2 error
   123  			authInfo, err2 = a.checkCreds(
   124  				a.statePool.SystemState(),
   125  				req, authTag, false, authenticator,
   126  			)
   127  			if err2 == nil && authInfo.Controller {
   128  				err = nil
   129  			}
   130  		}
   131  		if err != nil {
   132  			return httpcontext.AuthInfo{}, errors.NewUnauthorized(err, "")
   133  		}
   134  	}
   135  	return authInfo, nil
   136  }
   137  
   138  func (a *Authenticator) checkCreds(
   139  	st *state.State,
   140  	req params.LoginRequest,
   141  	authTag names.Tag,
   142  	userLogin bool,
   143  	authenticator authentication.EntityAuthenticator,
   144  ) (httpcontext.AuthInfo, error) {
   145  	var entityFinder authentication.EntityFinder = st
   146  	if userLogin {
   147  		// When looking up model users, use a custom
   148  		// entity finder that looks up both the local user (if the user
   149  		// tag is in the local domain) and the model user.
   150  		entityFinder = modelUserEntityFinder{st}
   151  	}
   152  	entity, err := authenticator.Authenticate(entityFinder, authTag, req)
   153  	if err != nil {
   154  		return httpcontext.AuthInfo{}, errors.Trace(err)
   155  	}
   156  
   157  	authInfo := httpcontext.AuthInfo{Entity: entity}
   158  	type withIsManager interface {
   159  		IsManager() bool
   160  	}
   161  	if entity, ok := entity.(withIsManager); ok {
   162  		authInfo.Controller = entity.IsManager()
   163  	}
   164  
   165  	type withLastLogin interface {
   166  		LastLogin() (time.Time, error)
   167  		UpdateLastLogin() error
   168  	}
   169  	if entity, ok := entity.(withLastLogin); ok {
   170  		lastLogin, err := entity.LastLogin()
   171  		if err == nil {
   172  			authInfo.LastConnection = lastLogin
   173  		} else if !state.IsNeverLoggedInError(err) {
   174  			return httpcontext.AuthInfo{}, errors.Trace(err)
   175  		}
   176  		// TODO log or return error returned by
   177  		// UpdateLastLogin? Old code didn't do
   178  		// anything with it.
   179  		entity.UpdateLastLogin()
   180  	}
   181  	return authInfo, nil
   182  }
   183  
   184  // LoginRequest extracts basic auth login details from an http.Request.
   185  //
   186  // TODO(axw) we shouldn't be using params types here.
   187  func LoginRequest(req *http.Request) (params.LoginRequest, error) {
   188  	authHeader := req.Header.Get("Authorization")
   189  	macaroons := httpbakery.RequestMacaroons(req)
   190  	if authHeader == "" {
   191  		return params.LoginRequest{Macaroons: macaroons}, nil
   192  	}
   193  	parts := strings.Fields(authHeader)
   194  	if len(parts) != 2 || parts[0] != "Basic" {
   195  		// Invalid header format or no header provided.
   196  		return params.LoginRequest{}, errors.NotValidf("request format")
   197  	}
   198  
   199  	// Challenge is a base64-encoded "tag:pass" string.
   200  	// See RFC 2617, Section 2.
   201  	challenge, err := base64.StdEncoding.DecodeString(parts[1])
   202  	if err != nil {
   203  		return params.LoginRequest{}, errors.NotValidf("request format")
   204  	}
   205  	tagPass := strings.SplitN(string(challenge), ":", 2)
   206  	if len(tagPass) != 2 {
   207  		return params.LoginRequest{}, errors.NotValidf("request format")
   208  	}
   209  
   210  	// Ensure that a sensible tag was passed.
   211  	if _, err := names.ParseTag(tagPass[0]); err != nil {
   212  		return params.LoginRequest{}, errors.Trace(err)
   213  	}
   214  	return params.LoginRequest{
   215  		AuthTag:     tagPass[0],
   216  		Credentials: tagPass[1],
   217  		Macaroons:   httpbakery.RequestMacaroons(req),
   218  		Nonce:       req.Header.Get(params.MachineNonceHeader),
   219  	}, nil
   220  }