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 }