15881: Add LDAP authentication option.
[arvados.git] / lib / controller / localdb / login_pam.go
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 package localdb
6
7 import (
8         "context"
9         "errors"
10         "fmt"
11         "net/http"
12         "strings"
13
14         "git.arvados.org/arvados.git/lib/controller/rpc"
15         "git.arvados.org/arvados.git/sdk/go/arvados"
16         "git.arvados.org/arvados.git/sdk/go/ctxlog"
17         "git.arvados.org/arvados.git/sdk/go/httpserver"
18         "github.com/msteinert/pam"
19         "github.com/sirupsen/logrus"
20 )
21
22 type pamLoginController struct {
23         Cluster    *arvados.Cluster
24         RailsProxy *railsProxy
25 }
26
27 func (ctrl *pamLoginController) Logout(ctx context.Context, opts arvados.LogoutOptions) (arvados.LogoutResponse, error) {
28         return noopLogout(ctrl.Cluster, opts)
29 }
30
31 func (ctrl *pamLoginController) Login(ctx context.Context, opts arvados.LoginOptions) (arvados.LoginResponse, error) {
32         return arvados.LoginResponse{}, errors.New("interactive login is not available")
33 }
34
35 func (ctrl *pamLoginController) UserAuthenticate(ctx context.Context, opts arvados.UserAuthenticateOptions) (arvados.APIClientAuthorization, error) {
36         errorMessage := ""
37         sentPassword := false
38         tx, err := pam.StartFunc(ctrl.Cluster.Login.PAMService, opts.Username, func(style pam.Style, message string) (string, error) {
39                 ctxlog.FromContext(ctx).Debugf("pam conversation: style=%v message=%q", style, message)
40                 switch style {
41                 case pam.ErrorMsg:
42                         ctxlog.FromContext(ctx).WithField("Message", message).Info("pam.ErrorMsg")
43                         errorMessage = message
44                         return "", nil
45                 case pam.TextInfo:
46                         ctxlog.FromContext(ctx).WithField("Message", message).Info("pam.TextInfo")
47                         return "", nil
48                 case pam.PromptEchoOn, pam.PromptEchoOff:
49                         sentPassword = true
50                         return opts.Password, nil
51                 default:
52                         return "", fmt.Errorf("unrecognized message style %d", style)
53                 }
54         })
55         if err != nil {
56                 return arvados.APIClientAuthorization{}, err
57         }
58         err = tx.Authenticate(pam.DisallowNullAuthtok)
59         if err != nil {
60                 err = fmt.Errorf("PAM: %s", err)
61                 if errorMessage != "" {
62                         // Perhaps the error message in the
63                         // conversation is helpful.
64                         err = fmt.Errorf("%s; %q", err, errorMessage)
65                 }
66                 if sentPassword {
67                         err = fmt.Errorf("%s (with username %q and password)", err, opts.Username)
68                 } else {
69                         // This might hint that the username was
70                         // invalid.
71                         err = fmt.Errorf("%s (with username %q; password was never requested by PAM service)", err, opts.Username)
72                 }
73                 return arvados.APIClientAuthorization{}, httpserver.ErrorWithStatus(err, http.StatusUnauthorized)
74         }
75         if errorMessage != "" {
76                 return arvados.APIClientAuthorization{}, httpserver.ErrorWithStatus(errors.New(errorMessage), http.StatusUnauthorized)
77         }
78         user, err := tx.GetItem(pam.User)
79         if err != nil {
80                 return arvados.APIClientAuthorization{}, err
81         }
82         email := user
83         if domain := ctrl.Cluster.Login.PAMDefaultEmailDomain; domain != "" && !strings.Contains(email, "@") {
84                 email = email + "@" + domain
85         }
86         ctxlog.FromContext(ctx).WithFields(logrus.Fields{
87                 "user":  user,
88                 "email": email,
89         }).Debug("pam authentication succeeded")
90         return createAPIClientAuthorization(ctx, ctrl.RailsProxy, ctrl.Cluster.SystemRootToken, rpc.UserSessionAuthInfo{
91                 Username: user,
92                 Email:    email,
93         })
94 }