16212: Add pam_ldap test.
[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         "fmt"
10         "net/url"
11         "strings"
12
13         "git.arvados.org/arvados.git/lib/controller/rpc"
14         "git.arvados.org/arvados.git/sdk/go/arvados"
15         "git.arvados.org/arvados.git/sdk/go/auth"
16         "git.arvados.org/arvados.git/sdk/go/ctxlog"
17         "github.com/msteinert/pam"
18         "github.com/sirupsen/logrus"
19 )
20
21 type pamLoginController struct {
22         Cluster    *arvados.Cluster
23         RailsProxy *railsProxy
24 }
25
26 func (ctrl *pamLoginController) Logout(ctx context.Context, opts arvados.LogoutOptions) (arvados.LogoutResponse, error) {
27         return noopLogout(ctrl.Cluster, opts)
28 }
29
30 func (ctrl *pamLoginController) Login(ctx context.Context, opts arvados.LoginOptions) (arvados.LoginResponse, error) {
31         errorMessage := ""
32         tx, err := pam.StartFunc(ctrl.Cluster.Login.PAMService, opts.Username, func(style pam.Style, message string) (string, error) {
33                 ctxlog.FromContext(ctx).Debugf("pam conversation: style=%v message=%q", style, message)
34                 switch style {
35                 case pam.ErrorMsg:
36                         ctxlog.FromContext(ctx).WithField("Message", message).Info("pam.ErrorMsg")
37                         errorMessage = message
38                         return "", nil
39                 case pam.TextInfo:
40                         ctxlog.FromContext(ctx).WithField("Message", message).Info("pam.TextInfo")
41                         return "", nil
42                 case pam.PromptEchoOn, pam.PromptEchoOff:
43                         return opts.Password, nil
44                 default:
45                         return "", fmt.Errorf("unrecognized message style %d", style)
46                 }
47         })
48         if err != nil {
49                 return arvados.LoginResponse{Message: err.Error()}, nil
50         }
51         err = tx.Authenticate(pam.DisallowNullAuthtok)
52         if err != nil {
53                 return arvados.LoginResponse{Message: err.Error()}, nil
54         }
55         if errorMessage != "" {
56                 return arvados.LoginResponse{Message: errorMessage}, nil
57         }
58         user, err := tx.GetItem(pam.User)
59         if err != nil {
60                 return arvados.LoginResponse{Message: err.Error()}, nil
61         }
62         email := user
63         if domain := ctrl.Cluster.Login.PAMDefaultEmailDomain; domain != "" && !strings.Contains(email, "@") {
64                 email = email + "@" + domain
65         }
66         ctxlog.FromContext(ctx).WithFields(logrus.Fields{"user": user, "email": email}).Debug("pam authentication succeeded")
67         ctxRoot := auth.NewContext(ctx, &auth.Credentials{Tokens: []string{ctrl.Cluster.SystemRootToken}})
68         resp, err := ctrl.RailsProxy.UserSessionCreate(ctxRoot, rpc.UserSessionCreateOptions{
69                 // Send a fake ReturnTo value instead of the caller's
70                 // opts.ReturnTo. We won't follow the resulting
71                 // redirect target anyway.
72                 ReturnTo: opts.Remote + ",https://none.invalid",
73                 AuthInfo: rpc.UserSessionAuthInfo{
74                         Username: user,
75                         Email:    email,
76                 },
77         })
78         if err != nil {
79                 return arvados.LoginResponse{Message: err.Error()}, nil
80         }
81         target, err := url.Parse(resp.RedirectLocation)
82         if err != nil {
83                 return arvados.LoginResponse{Message: err.Error()}, nil
84         }
85         resp.Token = target.Query().Get("api_token")
86         resp.RedirectLocation = ""
87         return resp, err
88 }