15348: 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
28         "context"
29         "errors"
30         "fmt"
31         "runtime"
32         "syscall"
33         "time"
34
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"
39 )
40
41 /*
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);
47 */
48 import "C"
49
50 func main() {}
51
52 func init() {
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")
55         }
56 }
57
58 //export pam_sm_setcred
59 func pam_sm_setcred(pamh *C.pam_handle_t, flags, cArgc C.int, cArgv **C.char) C.int {
60         return C.PAM_IGNORE
61 }
62
63 //export pam_sm_authenticate
64 func pam_sm_authenticate(pamh *C.pam_handle_t, flags, cArgc C.int, cArgv **C.char) C.int {
65         runtime.GOMAXPROCS(1)
66         logger := newLogger(flags&C.PAM_SILENT == 0)
67         cUsername := C.get_user(pamh)
68         if cUsername == nil {
69                 return C.PAM_USER_UNKNOWN
70         }
71
72         cToken := C.get_authtoken(pamh)
73         if cToken == nil {
74                 return C.PAM_AUTH_ERR
75         }
76
77         argv := make([]string, cArgc)
78         for i := 0; i < int(cArgc); i++ {
79                 argv[i] = C.GoString(C.stringindex(cArgv, C.int(i)))
80         }
81
82         err := authenticate(logger, C.GoString(cUsername), C.GoString(cToken), argv)
83         if err != nil {
84                 logger.WithError(err).Error("authentication failed")
85                 return C.PAM_AUTH_ERR
86         }
87         return C.PAM_SUCCESS
88 }
89
90 func authenticate(logger *logrus.Logger, username, token string, argv []string) error {
91         hostname := ""
92         apiHost := ""
93         insecure := false
94         for idx, arg := range argv {
95                 if idx == 0 {
96                         apiHost = arg
97                 } else if idx == 1 {
98                         hostname = arg
99                 } else if arg == "insecure" {
100                         insecure = true
101                 } else if arg == "debug" {
102                         logger.SetLevel(logrus.DebugLevel)
103                 } else {
104                         logger.Warnf("unkown option: %s\n", arg)
105                 }
106         }
107         logger.Debugf("username=%q arvados_api_host=%q hostname=%q insecure=%t", username, apiHost, hostname, insecure)
108         if apiHost == "" || hostname == "" {
109                 logger.Warnf("cannot authenticate: config error: arvados_api_host and hostname must be non-empty")
110                 return errors.New("config error")
111         }
112         arv := &arvados.Client{
113                 Scheme:    "https",
114                 APIHost:   apiHost,
115                 AuthToken: token,
116                 Insecure:  insecure,
117         }
118         ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(time.Minute))
119         defer cancel()
120         var vms arvados.VirtualMachineList
121         err := arv.RequestAndDecodeContext(ctx, &vms, "GET", "arvados/v1/virtual_machines", nil, arvados.ListOptions{
122                 Limit: 2,
123                 Filters: []arvados.Filter{
124                         {"hostname", "=", hostname},
125                 },
126         })
127         if err != nil {
128                 return err
129         }
130         if len(vms.Items) == 0 {
131                 // It's possible there is no VM entry for the
132                 // configured hostname, but typically this just means
133                 // the user does not have permission to see (let alone
134                 // log in to) this VM.
135                 return errors.New("permission denied")
136         } else if len(vms.Items) > 1 {
137                 return fmt.Errorf("multiple results for hostname %q", hostname)
138         } else if vms.Items[0].Hostname != hostname {
139                 return fmt.Errorf("looked up hostname %q but controller returned record with hostname %q", hostname, vms.Items[0].Hostname)
140         }
141         var user arvados.User
142         err = arv.RequestAndDecodeContext(ctx, &user, "GET", "arvados/v1/users/current", nil, nil)
143         if err != nil {
144                 return err
145         }
146         var links arvados.LinkList
147         err = arv.RequestAndDecodeContext(ctx, &links, "GET", "arvados/v1/links", nil, arvados.ListOptions{
148                 Limit: 1,
149                 Filters: []arvados.Filter{
150                         {"link_class", "=", "permission"},
151                         {"name", "=", "can_login"},
152                         {"tail_uuid", "=", user.UUID},
153                         {"head_uuid", "=", vms.Items[0].UUID},
154                         {"properties.username", "=", username},
155                 },
156         })
157         if err != nil {
158                 return err
159         }
160         if len(links.Items) < 1 || links.Items[0].Properties["username"] != username {
161                 return errors.New("permission denied")
162         }
163         logger.Debugf("permission granted based on link with UUID %s", links.Items[0].UUID)
164         return nil
165 }
166
167 func newLogger(stderr bool) *logrus.Logger {
168         logger := logrus.New()
169         if !stderr {
170                 logger.Out = ioutil.Discard
171         }
172         if hook, err := lSyslog.NewSyslogHook("udp", "localhost:514", syslog.LOG_AUTH|syslog.LOG_INFO, "pam_arvados"); err != nil {
173                 logger.Hooks.Add(hook)
174         }
175         return logger
176 }