1 // Copyright (C) The Arvados Authors. All rights reserved.
3 // SPDX-License-Identifier: AGPL-3.0
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"
21 type pamLoginController struct {
22 Cluster *arvados.Cluster
23 RailsProxy *railsProxy
26 func (ctrl *pamLoginController) Logout(ctx context.Context, opts arvados.LogoutOptions) (arvados.LogoutResponse, error) {
27 return noopLogout(ctrl.Cluster, opts)
30 func (ctrl *pamLoginController) Login(ctx context.Context, opts arvados.LoginOptions) (arvados.LoginResponse, error) {
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)
36 ctxlog.FromContext(ctx).WithField("Message", message).Info("pam.ErrorMsg")
37 errorMessage = message
40 ctxlog.FromContext(ctx).WithField("Message", message).Info("pam.TextInfo")
42 case pam.PromptEchoOn, pam.PromptEchoOff:
43 return opts.Password, nil
45 return "", fmt.Errorf("unrecognized message style %d", style)
49 return arvados.LoginResponse{Message: err.Error()}, nil
51 err = tx.Authenticate(pam.DisallowNullAuthtok)
53 return arvados.LoginResponse{Message: err.Error()}, nil
55 if errorMessage != "" {
56 return arvados.LoginResponse{Message: errorMessage}, nil
58 user, err := tx.GetItem(pam.User)
60 return arvados.LoginResponse{Message: err.Error()}, nil
63 if domain := ctrl.Cluster.Login.PAMDefaultEmailDomain; domain != "" && !strings.Contains(email, "@") {
64 email = email + "@" + domain
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{
79 return arvados.LoginResponse{Message: err.Error()}, nil
81 target, err := url.Parse(resp.RedirectLocation)
83 return arvados.LoginResponse{Message: err.Error()}, nil
85 resp.Token = target.Query().Get("api_token")
86 resp.RedirectLocation = ""