// Copyright (C) The Arvados Authors. All rights reserved. // // SPDX-License-Identifier: Apache-2.0 // To enable, add an entry in /etc/pam.d/common-auth where pam_unix.so // would normally be. Examples: // // auth [success=1 default=ignore] /usr/lib/pam_arvados.so zzzzz.arvadosapi.com vmhostname.example // auth [success=1 default=ignore] /usr/lib/pam_arvados.so zzzzz.arvadosapi.com vmhostname.example insecure debug // // Replace zzzzz.arvadosapi.com with your controller host or // host:port. // // Replace vmhostname.example with the VM's name as it appears in the // Arvados virtual_machine object. // // Use "insecure" if your API server certificate does not pass name // verification. // // Use "debug" to enable debug log messages. package main import ( "io/ioutil" "log/syslog" "os" "context" "errors" "fmt" "runtime" "syscall" "time" "git.arvados.org/arvados.git/sdk/go/arvados" "github.com/sirupsen/logrus" lSyslog "github.com/sirupsen/logrus/hooks/syslog" "golang.org/x/sys/unix" ) /* #cgo LDFLAGS: -lpam -fPIC #include char *stringindex(char** a, int i); const char *get_user(pam_handle_t *pamh); const char *get_authtoken(pam_handle_t *pamh); */ import "C" func main() {} func init() { if err := unix.Prctl(syscall.PR_SET_DUMPABLE, 0, 0, 0, 0); err != nil { newLogger(false).WithError(err).Warn("unable to disable ptrace") } } //export pam_sm_setcred func pam_sm_setcred(pamh *C.pam_handle_t, flags, cArgc C.int, cArgv **C.char) C.int { return C.PAM_IGNORE } //export pam_sm_authenticate func pam_sm_authenticate(pamh *C.pam_handle_t, flags, cArgc C.int, cArgv **C.char) C.int { runtime.GOMAXPROCS(1) logger := newLogger(flags&C.PAM_SILENT == 0) cUsername := C.get_user(pamh) if cUsername == nil { return C.PAM_USER_UNKNOWN } cToken := C.get_authtoken(pamh) if cToken == nil { return C.PAM_AUTH_ERR } argv := make([]string, cArgc) for i := 0; i < int(cArgc); i++ { argv[i] = C.GoString(C.stringindex(cArgv, C.int(i))) } err := authenticate(logger, C.GoString(cUsername), C.GoString(cToken), argv) if err != nil { logger.WithError(err).Error("authentication failed") return C.PAM_AUTH_ERR } return C.PAM_SUCCESS } func authenticate(logger *logrus.Logger, username, token string, argv []string) error { hostname := "" apiHost := "" insecure := false for idx, arg := range argv { if idx == 0 { apiHost = arg } else if idx == 1 { hostname = arg } else if arg == "insecure" { insecure = true } else if arg == "debug" { logger.SetLevel(logrus.DebugLevel) } else { logger.Warnf("unknown option: %s\n", arg) } } if hostname == "" || hostname == "-" { h, err := os.Hostname() if err != nil { logger.WithError(err).Warnf("cannot get hostname -- try using an explicit hostname in pam config") return fmt.Errorf("cannot get hostname: %w", err) } hostname = h } logger.Debugf("username=%q arvados_api_host=%q hostname=%q insecure=%t", username, apiHost, hostname, insecure) if apiHost == "" { logger.Warnf("cannot authenticate: config error: arvados_api_host and hostname must be non-empty") return errors.New("config error") } arv := &arvados.Client{ Scheme: "https", APIHost: apiHost, AuthToken: token, Insecure: insecure, } ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(time.Minute)) defer cancel() var vms arvados.VirtualMachineList err := arv.RequestAndDecodeContext(ctx, &vms, "GET", "arvados/v1/virtual_machines", nil, arvados.ListOptions{ Limit: 2, Filters: []arvados.Filter{ {"hostname", "=", hostname}, }, }) if err != nil { return err } if len(vms.Items) == 0 { // It's possible there is no VM entry for the // configured hostname, but typically this just means // the user does not have permission to see (let alone // log in to) this VM. return errors.New("permission denied") } else if len(vms.Items) > 1 { return fmt.Errorf("multiple results for hostname %q", hostname) } else if vms.Items[0].Hostname != hostname { return fmt.Errorf("looked up hostname %q but controller returned record with hostname %q", hostname, vms.Items[0].Hostname) } var user arvados.User err = arv.RequestAndDecodeContext(ctx, &user, "GET", "arvados/v1/users/current", nil, nil) if err != nil { return err } var links arvados.LinkList err = arv.RequestAndDecodeContext(ctx, &links, "GET", "arvados/v1/links", nil, arvados.ListOptions{ Limit: 1, Filters: []arvados.Filter{ {"link_class", "=", "permission"}, {"name", "=", "can_login"}, {"tail_uuid", "=", user.UUID}, {"head_uuid", "=", vms.Items[0].UUID}, {"properties.username", "=", username}, }, }) if err != nil { return err } if len(links.Items) < 1 || links.Items[0].Properties["username"] != username { return errors.New("permission denied") } logger.Debugf("permission granted based on link with UUID %s", links.Items[0].UUID) return nil } func newLogger(stderr bool) *logrus.Logger { logger := logrus.New() if !stderr { logger.Out = ioutil.Discard } if hook, err := lSyslog.NewSyslogHook("udp", "localhost:514", syslog.LOG_AUTH|syslog.LOG_INFO, "pam_arvados"); err != nil { logger.Hooks.Add(hook) } return logger }