17208: Merge branch 'master'
[arvados.git] / lib / pam / pam_arvados.go
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: Apache-2.0
4
5 // To enable, add an entry in /etc/pam.d/common-auth where pam_unix.so
6 // would normally be. Examples:
7 //
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
10 //
11 // Replace zzzzz.arvadosapi.com with your controller host or
12 // host:port.
13 //
14 // Replace vmhostname.example with the VM's name as it appears in the
15 // Arvados virtual_machine object.
16 //
17 // Use "insecure" if your API server certificate does not pass name
18 // verification.
19 //
20 // Use "debug" to enable debug log messages.
21
22 package main
23
24 import (
25         "io/ioutil"
26         "log/syslog"
27         "os"
28
29         "context"
30         "errors"
31         "fmt"
32         "runtime"
33         "syscall"
34         "time"
35
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"
40 )
41
42 /*
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);
48 */
49 import "C"
50
51 func main() {}
52
53 func init() {
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")
56         }
57 }
58
59 //export pam_sm_setcred
60 func pam_sm_setcred(pamh *C.pam_handle_t, flags, cArgc C.int, cArgv **C.char) C.int {
61         return C.PAM_IGNORE
62 }
63
64 //export pam_sm_authenticate
65 func pam_sm_authenticate(pamh *C.pam_handle_t, flags, cArgc C.int, cArgv **C.char) C.int {
66         runtime.GOMAXPROCS(1)
67         logger := newLogger(flags&C.PAM_SILENT == 0)
68         cUsername := C.get_user(pamh)
69         if cUsername == nil {
70                 return C.PAM_USER_UNKNOWN
71         }
72
73         cToken := C.get_authtoken(pamh)
74         if cToken == nil {
75                 return C.PAM_AUTH_ERR
76         }
77
78         argv := make([]string, cArgc)
79         for i := 0; i < int(cArgc); i++ {
80                 argv[i] = C.GoString(C.stringindex(cArgv, C.int(i)))
81         }
82
83         err := authenticate(logger, C.GoString(cUsername), C.GoString(cToken), argv)
84         if err != nil {
85                 logger.WithError(err).Error("authentication failed")
86                 return C.PAM_AUTH_ERR
87         }
88         return C.PAM_SUCCESS
89 }
90
91 func authenticate(logger *logrus.Logger, username, token string, argv []string) error {
92         hostname := ""
93         apiHost := ""
94         insecure := false
95         for idx, arg := range argv {
96                 if idx == 0 {
97                         apiHost = arg
98                 } else if idx == 1 {
99                         hostname = arg
100                 } else if arg == "insecure" {
101                         insecure = true
102                 } else if arg == "debug" {
103                         logger.SetLevel(logrus.DebugLevel)
104                 } else {
105                         logger.Warnf("unknown option: %s\n", arg)
106                 }
107         }
108         if hostname == "" || hostname == "-" {
109                 h, err := os.Hostname()
110                 if err != nil {
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)
113                 }
114                 hostname = h
115         }
116         logger.Debugf("username=%q arvados_api_host=%q hostname=%q insecure=%t", username, apiHost, hostname, insecure)
117         if apiHost == "" {
118                 logger.Warnf("cannot authenticate: config error: arvados_api_host and hostname must be non-empty")
119                 return errors.New("config error")
120         }
121         arv := &arvados.Client{
122                 Scheme:    "https",
123                 APIHost:   apiHost,
124                 AuthToken: token,
125                 Insecure:  insecure,
126         }
127         ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(time.Minute))
128         defer cancel()
129         var vms arvados.VirtualMachineList
130         err := arv.RequestAndDecodeContext(ctx, &vms, "GET", "arvados/v1/virtual_machines", nil, arvados.ListOptions{
131                 Limit: 2,
132                 Filters: []arvados.Filter{
133                         {"hostname", "=", hostname},
134                 },
135         })
136         if err != nil {
137                 return err
138         }
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)
149         }
150         var user arvados.User
151         err = arv.RequestAndDecodeContext(ctx, &user, "GET", "arvados/v1/users/current", nil, nil)
152         if err != nil {
153                 return err
154         }
155         var links arvados.LinkList
156         err = arv.RequestAndDecodeContext(ctx, &links, "GET", "arvados/v1/links", nil, arvados.ListOptions{
157                 Limit: 1,
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},
164                 },
165         })
166         if err != nil {
167                 return err
168         }
169         if len(links.Items) < 1 || links.Items[0].Properties["username"] != username {
170                 return errors.New("permission denied")
171         }
172         logger.Debugf("permission granted based on link with UUID %s", links.Items[0].UUID)
173         return nil
174 }
175
176 func newLogger(stderr bool) *logrus.Logger {
177         logger := logrus.New()
178         if !stderr {
179                 logger.Out = ioutil.Discard
180         }
181         if hook, err := lSyslog.NewSyslogHook("udp", "localhost:514", syslog.LOG_AUTH|syslog.LOG_INFO, "pam_arvados"); err != nil {
182                 logger.Hooks.Add(hook)
183         }
184         return logger
185 }