21700: Install Bundler system-wide in Rails postinst
[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 //go:build !static
6
7 package localdb
8
9 import (
10         "context"
11         "errors"
12         "fmt"
13         "net/http"
14         "strings"
15
16         "git.arvados.org/arvados.git/lib/controller/rpc"
17         "git.arvados.org/arvados.git/sdk/go/arvados"
18         "git.arvados.org/arvados.git/sdk/go/ctxlog"
19         "git.arvados.org/arvados.git/sdk/go/httpserver"
20         "github.com/msteinert/pam"
21         "github.com/sirupsen/logrus"
22 )
23
24 type pamLoginController struct {
25         Cluster *arvados.Cluster
26         Parent  *Conn
27 }
28
29 func (ctrl *pamLoginController) Logout(ctx context.Context, opts arvados.LogoutOptions) (arvados.LogoutResponse, error) {
30         return logout(ctx, ctrl.Cluster, opts)
31 }
32
33 func (ctrl *pamLoginController) Login(ctx context.Context, opts arvados.LoginOptions) (arvados.LoginResponse, error) {
34         return arvados.LoginResponse{}, errors.New("interactive login is not available")
35 }
36
37 func (ctrl *pamLoginController) UserAuthenticate(ctx context.Context, opts arvados.UserAuthenticateOptions) (arvados.APIClientAuthorization, error) {
38         errorMessage := ""
39         sentPassword := false
40         tx, err := pam.StartFunc(ctrl.Cluster.Login.PAM.Service, opts.Username, func(style pam.Style, message string) (string, error) {
41                 ctxlog.FromContext(ctx).Debugf("pam conversation: style=%v message=%q", style, message)
42                 switch style {
43                 case pam.ErrorMsg:
44                         ctxlog.FromContext(ctx).WithField("Message", message).Info("pam.ErrorMsg")
45                         errorMessage = message
46                         return "", nil
47                 case pam.TextInfo:
48                         ctxlog.FromContext(ctx).WithField("Message", message).Info("pam.TextInfo")
49                         return "", nil
50                 case pam.PromptEchoOn, pam.PromptEchoOff:
51                         sentPassword = true
52                         return opts.Password, nil
53                 default:
54                         return "", fmt.Errorf("unrecognized message style %d", style)
55                 }
56         })
57         if err != nil {
58                 return arvados.APIClientAuthorization{}, err
59         }
60         // Check that the given credentials are valid.
61         err = tx.Authenticate(pam.DisallowNullAuthtok)
62         if err != nil {
63                 err = fmt.Errorf("PAM: %s", err)
64                 if errorMessage != "" {
65                         // Perhaps the error message in the
66                         // conversation is helpful.
67                         err = fmt.Errorf("%s; %q", err, errorMessage)
68                 }
69                 if sentPassword {
70                         err = fmt.Errorf("%s (with username %q and password)", err, opts.Username)
71                 } else {
72                         // This might hint that the username was
73                         // invalid.
74                         err = fmt.Errorf("%s (with username %q; password was never requested by PAM service)", err, opts.Username)
75                 }
76                 return arvados.APIClientAuthorization{}, httpserver.ErrorWithStatus(err, http.StatusUnauthorized)
77         }
78         if errorMessage != "" {
79                 return arvados.APIClientAuthorization{}, httpserver.ErrorWithStatus(errors.New(errorMessage), http.StatusUnauthorized)
80         }
81         // Check that the account/user is permitted to access this host.
82         err = tx.AcctMgmt(pam.DisallowNullAuthtok)
83         if err != nil {
84                 err = fmt.Errorf("PAM: %s", err)
85                 if errorMessage != "" {
86                         err = fmt.Errorf("%s; %q", err, errorMessage)
87                 }
88                 return arvados.APIClientAuthorization{}, httpserver.ErrorWithStatus(err, http.StatusUnauthorized)
89         }
90         user, err := tx.GetItem(pam.User)
91         if err != nil {
92                 return arvados.APIClientAuthorization{}, err
93         }
94         email := user
95         if domain := ctrl.Cluster.Login.PAM.DefaultEmailDomain; domain != "" && !strings.Contains(email, "@") {
96                 email = email + "@" + domain
97         }
98         ctxlog.FromContext(ctx).WithFields(logrus.Fields{
99                 "user":  user,
100                 "email": email,
101         }).Debug("pam authentication succeeded")
102         return ctrl.Parent.CreateAPIClientAuthorization(ctx, ctrl.Cluster.SystemRootToken, rpc.UserSessionAuthInfo{
103                 Username: user,
104                 Email:    email,
105         })
106 }