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.
35 "git.arvados.org/arvados.git/sdk/go/arvados"
36 "github.com/sirupsen/logrus"
37 lSyslog "github.com/sirupsen/logrus/hooks/syslog"
38 "golang.org/x/sys/unix"
42 #cgo LDFLAGS: -lpam -fPIC
43 #include <security/pam_ext.h>
44 char *stringindex(char** a, int i);
45 const char *get_user(pam_handle_t *pamh);
46 const char *get_authtoken(pam_handle_t *pamh);
53 if err := unix.Prctl(syscall.PR_SET_DUMPABLE, 0, 0, 0, 0); err != nil {
54 newLogger(false).WithError(err).Warn("unable to disable ptrace")
58 //export pam_sm_authenticate
59 func pam_sm_authenticate(pamh *C.pam_handle_t, flags, cArgc C.int, cArgv **C.char) C.int {
61 logger := newLogger(flags&C.PAM_SILENT == 0)
62 cUsername := C.get_user(pamh)
64 return C.PAM_USER_UNKNOWN
67 cToken := C.get_authtoken(pamh)
72 argv := make([]string, cArgc)
73 for i := 0; i < int(cArgc); i++ {
74 argv[i] = C.GoString(C.stringindex(cArgv, C.int(i)))
77 err := authenticate(logger, C.GoString(cUsername), C.GoString(cToken), argv)
79 logger.WithError(err).Error("authentication failed")
85 func authenticate(logger *logrus.Logger, username, token string, argv []string) error {
89 for idx, arg := range argv {
94 } else if arg == "insecure" {
96 } else if arg == "debug" {
97 logger.SetLevel(logrus.DebugLevel)
99 logger.Warnf("unkown option: %s\n", arg)
102 logger.Debugf("username=%q arvados_api_host=%q hostname=%q insecure=%t", username, apiHost, hostname, insecure)
103 if apiHost == "" || hostname == "" {
104 logger.Warnf("cannot authenticate: config error: arvados_api_host and hostname must be non-empty")
105 return errors.New("config error")
107 arv := &arvados.Client{
113 ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(time.Minute))
115 var vms arvados.VirtualMachineList
116 err := arv.RequestAndDecodeContext(ctx, &vms, "GET", "arvados/v1/virtual_machines", nil, arvados.ListOptions{
118 Filters: []arvados.Filter{
119 {"hostname", "=", hostname},
125 if len(vms.Items) == 0 {
126 return fmt.Errorf("no results for hostname %q", hostname)
127 } else if len(vms.Items) > 1 {
128 return fmt.Errorf("multiple results for hostname %q", hostname)
129 } else if vms.Items[0].Hostname != hostname {
130 return fmt.Errorf("looked up hostname %q but controller returned record with hostname %q", hostname, vms.Items[0].Hostname)
132 var user arvados.User
133 err = arv.RequestAndDecodeContext(ctx, &user, "GET", "arvados/v1/users/current", nil, nil)
137 var links arvados.LinkList
138 err = arv.RequestAndDecodeContext(ctx, &links, "GET", "arvados/v1/links", nil, arvados.ListOptions{
140 Filters: []arvados.Filter{
141 {"link_class", "=", "permission"},
142 {"name", "=", "can_login"},
143 {"tail_uuid", "=", user.UUID},
144 {"head_uuid", "=", vms.Items[0].UUID},
145 {"properties.username", "=", username},
151 if len(links.Items) < 1 || links.Items[0].Properties["username"] != username {
152 return errors.New("permission denied")
154 logger.Debugf("permission granted based on link with UUID %s", links.Items[0].UUID)
158 func newLogger(stderr bool) *logrus.Logger {
159 logger := logrus.New()
161 logger.Out = ioutil.Discard
163 if hook, err := lSyslog.NewSyslogHook("udp", "localhost:514", syslog.LOG_AUTH|syslog.LOG_INFO, "pam_arvados"); err != nil {
164 logger.Hooks.Add(hook)