github.com/treeverse/lakefs@v1.24.1-0.20240520134607-95648127bfb0/pkg/auth/remoteauthenticator/authenticator.go (about)

     1  package remoteauthenticator
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"encoding/json"
     7  	"errors"
     8  	"fmt"
     9  	"io"
    10  	"net/http"
    11  	"time"
    12  
    13  	"github.com/go-openapi/swag"
    14  	"github.com/treeverse/lakefs/pkg/auth"
    15  	"github.com/treeverse/lakefs/pkg/auth/model"
    16  	"github.com/treeverse/lakefs/pkg/logging"
    17  )
    18  
    19  const remoteAuthSource = "remote_authenticator"
    20  
    21  var ErrBadConfig = errors.New("invalid configuration")
    22  
    23  // AuthenticatorConfig holds authentication configuration.
    24  type AuthenticatorConfig struct {
    25  	// Enabled if set true will enable authenticator
    26  	Enabled bool
    27  	// Endpoint URL of the remote authentication service (e.g. https://my-auth.example.com/auth)
    28  	Endpoint string
    29  	// DefaultUserGroup is the default group for the users authenticated by the remote service
    30  	DefaultUserGroup string
    31  	// RequestTimeout timeout for remote authentication requests
    32  	RequestTimeout time.Duration
    33  }
    34  
    35  // AuthenticationRequest is the request object that will be sent to the remote authenticator service as JSON payload in a POST request
    36  type AuthenticationRequest struct {
    37  	Username string `json:"username"`
    38  	Password string `json:"password"`
    39  }
    40  
    41  // AuthenticationResponse is the expected response from the remote authenticator service
    42  type AuthenticationResponse struct {
    43  	// ExternalUserIdentifier is optional, if returned then the user will be used as the official username in lakeFS
    44  	ExternalUserIdentifier *string `json:"external_user_identifier,omitempty"`
    45  }
    46  
    47  // Authenticator client
    48  type Authenticator struct {
    49  	AuthService auth.Service
    50  	Logger      logging.Logger
    51  	Config      AuthenticatorConfig
    52  	client      *http.Client
    53  }
    54  
    55  func NewAuthenticator(conf AuthenticatorConfig, authService auth.Service, logger logging.Logger) (*Authenticator, error) {
    56  	if conf.Endpoint == "" {
    57  		return nil, fmt.Errorf("endpoint is empty: %w", ErrBadConfig)
    58  	}
    59  
    60  	httpClient := &http.Client{Timeout: conf.RequestTimeout}
    61  
    62  	log := logger.WithField("service_name", remoteAuthSource)
    63  
    64  	log.WithFields(logging.Fields{
    65  		"auth_url":        conf.Endpoint,
    66  		"request_timeout": httpClient.Timeout,
    67  	}).Info("initializing remote authenticator")
    68  
    69  	return &Authenticator{
    70  		Logger:      log,
    71  		Config:      conf,
    72  		AuthService: authService,
    73  		client:      httpClient,
    74  	}, nil
    75  }
    76  
    77  func (ra *Authenticator) doRequest(ctx context.Context, log logging.Logger, username, password string) (*AuthenticationResponse, error) {
    78  	payload, err := json.Marshal(&AuthenticationRequest{Username: username, Password: password})
    79  	if err != nil {
    80  		return nil, fmt.Errorf("failed marshaling request body: %w", err)
    81  	}
    82  
    83  	req, err := http.NewRequestWithContext(ctx, http.MethodPost, ra.Config.Endpoint, bytes.NewReader(payload))
    84  	if err != nil {
    85  		return nil, fmt.Errorf("failed creating request to remote authenticator: %w", err)
    86  	}
    87  
    88  	req.Header.Set("Content-Type", "application/json")
    89  	log = log.WithField("url", req.URL.String())
    90  
    91  	log.Trace("starting http request to remote authenticator")
    92  
    93  	resp, err := ra.client.Do(req)
    94  	if err != nil {
    95  		return nil, fmt.Errorf("failed sending request to remote authenticator: %w", err)
    96  	}
    97  	defer func() { _ = resp.Body.Close() }()
    98  
    99  	log = log.WithField("status_code", resp.StatusCode)
   100  
   101  	if resp.StatusCode < http.StatusOK || resp.StatusCode >= 300 {
   102  		return nil, fmt.Errorf("bad status code %d: %w", resp.StatusCode, auth.ErrUnexpectedStatusCode)
   103  	}
   104  
   105  	log.Debug("got response from remote authenticator")
   106  
   107  	body, err := io.ReadAll(resp.Body)
   108  	if err != nil {
   109  		return nil, fmt.Errorf("failed reading response body: %w", err)
   110  	}
   111  
   112  	var res AuthenticationResponse
   113  	if err := json.Unmarshal(body, &res); err != nil {
   114  		return nil, fmt.Errorf("unmarshaling authenticator response %s: %w", username, err)
   115  	}
   116  
   117  	return &res, nil
   118  }
   119  
   120  func (ra *Authenticator) AuthenticateUser(ctx context.Context, username, password string) (string, error) {
   121  	log := ra.Logger.WithContext(ctx).WithField("input_username", username)
   122  
   123  	res, err := ra.doRequest(ctx, log, username, password)
   124  	if err != nil {
   125  		return "", err
   126  	}
   127  
   128  	dbUsername := username
   129  
   130  	// if the external authentication service provided an external user identifier, use it as the username
   131  	externalUserIdentifier := swag.StringValue(res.ExternalUserIdentifier)
   132  	if externalUserIdentifier != "" {
   133  		log = log.WithField("external_user_identifier", externalUserIdentifier)
   134  		dbUsername = externalUserIdentifier
   135  	}
   136  
   137  	user, err := ra.AuthService.GetUser(ctx, dbUsername)
   138  	if err == nil {
   139  		log.WithField("user", fmt.Sprintf("%+v", user)).Debug("Got existing user")
   140  		return user.Username, nil
   141  	}
   142  	if !errors.Is(err, auth.ErrNotFound) {
   143  		return "", fmt.Errorf("get user %s: %w", dbUsername, err)
   144  	}
   145  
   146  	log.Info("first time remote authenticated user, creating them")
   147  
   148  	newUser := &model.User{
   149  		CreatedAt:    time.Now().UTC(),
   150  		Username:     dbUsername,
   151  		FriendlyName: &username,
   152  		Source:       remoteAuthSource,
   153  	}
   154  
   155  	_, err = ra.AuthService.CreateUser(ctx, newUser)
   156  	if err != nil {
   157  		return "", fmt.Errorf("create backing user for remote auth user %s: %w", newUser.Username, err)
   158  	}
   159  
   160  	err = ra.AuthService.AddUserToGroup(ctx, newUser.Username, ra.Config.DefaultUserGroup)
   161  	if err != nil {
   162  		return "", fmt.Errorf("add newly created remote auth user %s to %s: %w", newUser.Username, ra.Config.DefaultUserGroup, err)
   163  	}
   164  	return newUser.Username, nil
   165  }
   166  
   167  func (ra *Authenticator) String() string {
   168  	return remoteAuthSource
   169  }