github.com/kiali/kiali@v1.84.0/business/authentication/header_auth_controller.go (about)

     1  package authentication
     2  
     3  import (
     4  	"net/http"
     5  	"strings"
     6  	"time"
     7  
     8  	"k8s.io/client-go/tools/clientcmd/api"
     9  
    10  	"github.com/kiali/kiali/config"
    11  	"github.com/kiali/kiali/kubernetes"
    12  	"github.com/kiali/kiali/log"
    13  	"github.com/kiali/kiali/util"
    14  )
    15  
    16  // headerAuthController contains the backing logic to implement
    17  // Kiali's "header" authentication strategy. It assumes that authentication
    18  // is fully done by an external system and Kiali does not participate. Kiali
    19  // receives already valid credentials through HTTP headers on each request.
    20  // Because of this, only minimal validation of the received credentials is
    21  // performed.
    22  type headerAuthController struct {
    23  	homeClusterSAClient kubernetes.ClientInterface
    24  	// SessionStore persists the session between HTTP requests.
    25  	SessionStore SessionPersistor
    26  }
    27  
    28  // headerSessionPayload is a helper type used as session data storage. An instance
    29  // of this type is used with the SessionPersistor for session creation and persistence.
    30  type headerSessionPayload struct {
    31  	// The resolved username associated with the received credentials.
    32  	Subject string `json:"subject,omitempty"`
    33  
    34  	// Token is the Bearer token that the upstream client sent on the HTTP Authorization
    35  	// header at the initial authentication.
    36  	Token string `json:"token,omitempty"`
    37  }
    38  
    39  // NewHeaderAuthController initializes a new controller for allowing already authenticated requests, with the
    40  // given persistor and the given businessInstantiator. The businessInstantiator can be nil and
    41  // the initialized controller will use the business.Get function.
    42  func NewHeaderAuthController(persistor SessionPersistor, homeClusterSAClient kubernetes.ClientInterface) *headerAuthController {
    43  	return &headerAuthController{
    44  		homeClusterSAClient: homeClusterSAClient,
    45  		SessionStore:        persistor,
    46  	}
    47  }
    48  
    49  // Authenticate handles an HTTP request that contains credentials passed in HTTP headers.
    50  // It is assumed that some external system is fully controlling authentication. Thus, it is
    51  // assumed that the received credentials should be valid. Nevertheless, a minimal verification
    52  // is done by trying to fetch the account/user name from the cluster. If account/user name information
    53  // cannot be fetched, authentication is rejected.
    54  // An error is returned if the authentication failed.
    55  func (c headerAuthController) Authenticate(r *http.Request, w http.ResponseWriter) (*UserSessionData, error) {
    56  	authInfo := c.getTokenStringFromHeader(r)
    57  
    58  	if authInfo == nil || authInfo.Token == "" {
    59  		c.SessionStore.TerminateSession(r, w)
    60  		return nil, &AuthenticationFailureError{
    61  			HttpStatus: http.StatusUnauthorized,
    62  			Reason:     "Token is missing",
    63  		}
    64  	}
    65  
    66  	// Get the subject for the token to validate it as a valid token
    67  	subjectFromToken, err := c.homeClusterSAClient.GetTokenSubject(authInfo)
    68  	if err != nil {
    69  		return nil, err
    70  	}
    71  
    72  	// The token has been validated via k8s TokenReview, extract the subject for the ui to display
    73  	// from either the subject (via the TokenReview) or the impersonation header
    74  	var tokenSubject string
    75  
    76  	if authInfo.Impersonate == "" {
    77  		tokenSubject = subjectFromToken
    78  		tokenSubject = strings.TrimPrefix(tokenSubject, "system:serviceaccount:") // Shorten the subject displayed in UI.
    79  	} else {
    80  		tokenSubject = authInfo.Impersonate
    81  	}
    82  
    83  	// Create the session
    84  	timeExpire := util.Clock.Now().Add(time.Second * time.Duration(config.Get().LoginToken.ExpirationSeconds))
    85  	err = c.SessionStore.CreateSession(r, w, config.AuthStrategyHeader, timeExpire, headerSessionPayload{Token: authInfo.Token, Subject: tokenSubject})
    86  	if err != nil {
    87  		return nil, err
    88  	}
    89  
    90  	return &UserSessionData{
    91  		ExpiresOn: timeExpire,
    92  		Username:  tokenSubject,
    93  		AuthInfo:  authInfo,
    94  	}, nil
    95  }
    96  
    97  // ValidateSession checks if credentials are available in HTTP headers. If they are present, a populated
    98  // UserSessionData is returned. Otherwise, nil is returned.
    99  func (c headerAuthController) ValidateSession(r *http.Request, w http.ResponseWriter) (*UserSessionData, error) {
   100  	log.Tracef("Using header for authentication, Url: [%s]", r.URL.String())
   101  
   102  	sPayload := headerSessionPayload{}
   103  	sData, err := c.SessionStore.ReadSession(r, w, &sPayload)
   104  	if err != nil {
   105  		log.Warningf("Could not read the session: %v", err)
   106  		return nil, err
   107  	}
   108  
   109  	authInfo := c.getTokenStringFromHeader(r)
   110  	if authInfo == nil || authInfo.Token == "" {
   111  		// No token in HTTP headers, means no session.
   112  		return nil, nil
   113  	}
   114  
   115  	// A token in HTTP headers means there is a valid session, even if our cookies have
   116  	// expired. So, if we have cookies, we can recover the subject. Else, send empty subject.
   117  	// Expiration time is probably irrelevant for this auth strategy, but to keep the so-so same behavior
   118  	// before the auth refactor, we set expiration time to "now" if we don't have cookies.
   119  	var expiration time.Time
   120  	var subject string
   121  	if sData == nil {
   122  		expiration = util.Clock.Now()
   123  		subject = ""
   124  	} else {
   125  		expiration = sData.ExpiresOn
   126  		subject = sPayload.Subject
   127  	}
   128  
   129  	return &UserSessionData{
   130  		ExpiresOn: expiration,
   131  		Username:  subject,
   132  		AuthInfo:  authInfo,
   133  	}, nil
   134  }
   135  
   136  // TerminateSession unconditionally terminates any existing session without any validation.
   137  func (c headerAuthController) TerminateSession(r *http.Request, w http.ResponseWriter) error {
   138  	c.SessionStore.TerminateSession(r, w)
   139  	return nil
   140  }
   141  
   142  // getTokenStringFromHeader builds a Kubernetes api.AuthInfo object that contains user credentials
   143  // and any other credential attributes received through HTTP headers. Minimally, the standard HTTP
   144  // Authorization header is required to be present in the request containing a Bearer token that
   145  // can be used to make requests to the cluster API. Additionally, Kubernetes Impersonation
   146  // headers are allowed. Since all these headers are going to be used against the cluster API, here
   147  // we read passively the data and let the cluster do its own validations on the credentials.
   148  func (c headerAuthController) getTokenStringFromHeader(r *http.Request) *api.AuthInfo {
   149  	tokenString := "" // Default to no token.
   150  
   151  	// Extract token from the Authorization HTTP header sent from the reverse proxy
   152  	if headerValue := r.Header.Get("Authorization"); strings.Contains(headerValue, "Bearer") {
   153  		tokenString = strings.TrimPrefix(headerValue, "Bearer ")
   154  	}
   155  
   156  	authInfo := &api.AuthInfo{Token: tokenString}
   157  
   158  	impersonationHeader := r.Header.Get("Impersonate-User")
   159  	if len(impersonationHeader) > 0 {
   160  		// there's an impersonation header, lets make sure to add it
   161  		authInfo.Impersonate = impersonationHeader
   162  
   163  		// Check for impersonated groups
   164  		if groupsImpersonationHeader := r.Header["Impersonate-Group"]; len(groupsImpersonationHeader) > 0 {
   165  			authInfo.ImpersonateGroups = groupsImpersonationHeader
   166  		}
   167  
   168  		// check for extra fields
   169  		for headerName, headerValues := range r.Header {
   170  			if strings.HasPrefix(headerName, "Impersonate-Extra-") {
   171  				extraName := headerName[len("Impersonate-Extra-"):]
   172  				if authInfo.ImpersonateUserExtra == nil {
   173  					authInfo.ImpersonateUserExtra = make(map[string][]string)
   174  				}
   175  				authInfo.ImpersonateUserExtra[extraName] = headerValues
   176  			}
   177  		}
   178  	}
   179  
   180  	return authInfo
   181  }