16212: Move user/pass authentication to its own endpoint.
[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         "net/url"
13         "strings"
14
15         "git.arvados.org/arvados.git/lib/controller/rpc"
16         "git.arvados.org/arvados.git/sdk/go/arvados"
17         "git.arvados.org/arvados.git/sdk/go/auth"
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         RailsProxy *railsProxy
27 }
28
29 func (ctrl *pamLoginController) Logout(ctx context.Context, opts arvados.LogoutOptions) (arvados.LogoutResponse, error) {
30         return noopLogout(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         tx, err := pam.StartFunc(ctrl.Cluster.Login.PAMService, opts.Username, func(style pam.Style, message string) (string, error) {
40                 ctxlog.FromContext(ctx).Debugf("pam conversation: style=%v message=%q", style, message)
41                 switch style {
42                 case pam.ErrorMsg:
43                         ctxlog.FromContext(ctx).WithField("Message", message).Info("pam.ErrorMsg")
44                         errorMessage = message
45                         return "", nil
46                 case pam.TextInfo:
47                         ctxlog.FromContext(ctx).WithField("Message", message).Info("pam.TextInfo")
48                         return "", nil
49                 case pam.PromptEchoOn, pam.PromptEchoOff:
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                 return arvados.APIClientAuthorization{}, httpserver.ErrorWithStatus(err, http.StatusUnauthorized)
61         }
62         if errorMessage != "" {
63                 return arvados.APIClientAuthorization{}, httpserver.ErrorWithStatus(errors.New(errorMessage), http.StatusUnauthorized)
64         }
65         user, err := tx.GetItem(pam.User)
66         if err != nil {
67                 return arvados.APIClientAuthorization{}, err
68         }
69         email := user
70         if domain := ctrl.Cluster.Login.PAMDefaultEmailDomain; domain != "" && !strings.Contains(email, "@") {
71                 email = email + "@" + domain
72         }
73         ctxlog.FromContext(ctx).WithFields(logrus.Fields{"user": user, "email": email}).Debug("pam authentication succeeded")
74         ctxRoot := auth.NewContext(ctx, &auth.Credentials{Tokens: []string{ctrl.Cluster.SystemRootToken}})
75         resp, err := ctrl.RailsProxy.UserSessionCreate(ctxRoot, rpc.UserSessionCreateOptions{
76                 // Send a fake ReturnTo value instead of the caller's
77                 // opts.ReturnTo. We won't follow the resulting
78                 // redirect target anyway.
79                 ReturnTo: ",https://none.invalid",
80                 AuthInfo: rpc.UserSessionAuthInfo{
81                         Username: user,
82                         Email:    email,
83                 },
84         })
85         if err != nil {
86                 return arvados.APIClientAuthorization{}, err
87         }
88         target, err := url.Parse(resp.RedirectLocation)
89         if err != nil {
90                 return arvados.APIClientAuthorization{}, err
91         }
92         return arvados.APIClientAuthorization{APIToken: target.Query().Get("api_token")}, err
93 }