1 // Copyright (C) The Arvados Authors. All rights reserved.
3 // SPDX-License-Identifier: Apache-2.0
5 // To enable, add an entry in /etc/pam.d/common-auth where pam_unix.so
6 // would normally be. Examples:
8 // auth [success=1 default=ignore] /usr/lib/pam_arvados.so zzzzz.arvadosapi.com vmhostname.example
9 // auth [success=1 default=ignore] /usr/lib/pam_arvados.so zzzzz.arvadosapi.com vmhostname.example insecure debug
11 // Replace zzzzz.arvadosapi.com with your controller host or
14 // Replace vmhostname.example with the VM's name as it appears in the
15 // Arvados virtual_machine object.
17 // Use "insecure" if your API server certificate does not pass name
20 // Use "debug" to enable debug log messages.
36 "git.arvados.org/arvados.git/sdk/go/arvados"
37 "github.com/sirupsen/logrus"
38 lSyslog "github.com/sirupsen/logrus/hooks/syslog"
39 "golang.org/x/sys/unix"
43 #cgo LDFLAGS: -lpam -fPIC
44 #include <security/pam_ext.h>
45 char *stringindex(char** a, int i);
46 const char *get_user(pam_handle_t *pamh);
47 const char *get_authtoken(pam_handle_t *pamh);
54 if err := unix.Prctl(syscall.PR_SET_DUMPABLE, 0, 0, 0, 0); err != nil {
55 newLogger(false).WithError(err).Warn("unable to disable ptrace")
59 //export pam_sm_setcred
60 func pam_sm_setcred(pamh *C.pam_handle_t, flags, cArgc C.int, cArgv **C.char) C.int {
64 //export pam_sm_authenticate
65 func pam_sm_authenticate(pamh *C.pam_handle_t, flags, cArgc C.int, cArgv **C.char) C.int {
67 logger := newLogger(flags&C.PAM_SILENT == 0)
68 cUsername := C.get_user(pamh)
70 return C.PAM_USER_UNKNOWN
73 cToken := C.get_authtoken(pamh)
78 argv := make([]string, cArgc)
79 for i := 0; i < int(cArgc); i++ {
80 argv[i] = C.GoString(C.stringindex(cArgv, C.int(i)))
83 err := authenticate(logger, C.GoString(cUsername), C.GoString(cToken), argv)
85 logger.WithError(err).Error("authentication failed")
91 func authenticate(logger *logrus.Logger, username, token string, argv []string) error {
95 for idx, arg := range argv {
100 } else if arg == "insecure" {
102 } else if arg == "debug" {
103 logger.SetLevel(logrus.DebugLevel)
105 logger.Warnf("unknown option: %s\n", arg)
108 if hostname == "" || hostname == "-" {
109 h, err := os.Hostname()
111 logger.WithError(err).Warnf("cannot get hostname -- try using an explicit hostname in pam config")
112 return fmt.Errorf("cannot get hostname: %w", err)
116 logger.Debugf("username=%q arvados_api_host=%q hostname=%q insecure=%t", username, apiHost, hostname, insecure)
118 logger.Warnf("cannot authenticate: config error: arvados_api_host and hostname must be non-empty")
119 return errors.New("config error")
121 arv := &arvados.Client{
127 ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(time.Minute))
129 var vms arvados.VirtualMachineList
130 err := arv.RequestAndDecodeContext(ctx, &vms, "GET", "arvados/v1/virtual_machines", nil, arvados.ListOptions{
132 Filters: []arvados.Filter{
133 {"hostname", "=", hostname},
139 if len(vms.Items) == 0 {
140 // It's possible there is no VM entry for the
141 // configured hostname, but typically this just means
142 // the user does not have permission to see (let alone
143 // log in to) this VM.
144 return errors.New("permission denied")
145 } else if len(vms.Items) > 1 {
146 return fmt.Errorf("multiple results for hostname %q", hostname)
147 } else if vms.Items[0].Hostname != hostname {
148 return fmt.Errorf("looked up hostname %q but controller returned record with hostname %q", hostname, vms.Items[0].Hostname)
150 var user arvados.User
151 err = arv.RequestAndDecodeContext(ctx, &user, "GET", "arvados/v1/users/current", nil, nil)
155 var links arvados.LinkList
156 err = arv.RequestAndDecodeContext(ctx, &links, "GET", "arvados/v1/links", nil, arvados.ListOptions{
158 Filters: []arvados.Filter{
159 {"link_class", "=", "permission"},
160 {"name", "=", "can_login"},
161 {"tail_uuid", "=", user.UUID},
162 {"head_uuid", "=", vms.Items[0].UUID},
163 {"properties.username", "=", username},
169 if len(links.Items) < 1 || links.Items[0].Properties["username"] != username {
170 return errors.New("permission denied")
172 logger.Debugf("permission granted based on link with UUID %s", links.Items[0].UUID)
176 func newLogger(stderr bool) *logrus.Logger {
177 logger := logrus.New()
179 logger.Out = ioutil.Discard
181 if hook, err := lSyslog.NewSyslogHook("udp", "localhost:514", syslog.LOG_AUTH|syslog.LOG_INFO, "pam_arvados"); err != nil {
182 logger.Hooks.Add(hook)