15348: Add debug flag & installation note.
[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_authenticate
59 func pam_sm_authenticate(pamh *C.pam_handle_t, flags, cArgc C.int, cArgv **C.char) C.int {
60         runtime.GOMAXPROCS(1)
61         logger := newLogger(flags&C.PAM_SILENT == 0)
62         cUsername := C.get_user(pamh)
63         if cUsername == nil {
64                 return C.PAM_USER_UNKNOWN
65         }
66
67         cToken := C.get_authtoken(pamh)
68         if cToken == nil {
69                 return C.PAM_AUTH_ERR
70         }
71
72         argv := make([]string, cArgc)
73         for i := 0; i < int(cArgc); i++ {
74                 argv[i] = C.GoString(C.stringindex(cArgv, C.int(i)))
75         }
76
77         err := authenticate(logger, C.GoString(cUsername), C.GoString(cToken), argv)
78         if err != nil {
79                 logger.WithError(err).Error("authentication failed")
80                 return C.PAM_AUTH_ERR
81         }
82         return C.PAM_SUCCESS
83 }
84
85 func authenticate(logger *logrus.Logger, username, token string, argv []string) error {
86         hostname := ""
87         apiHost := ""
88         insecure := false
89         for idx, arg := range argv {
90                 if idx == 0 {
91                         apiHost = arg
92                 } else if idx == 1 {
93                         hostname = arg
94                 } else if arg == "insecure" {
95                         insecure = true
96                 } else if arg == "debug" {
97                         logger.SetLevel(logrus.DebugLevel)
98                 } else {
99                         logger.Warnf("unkown option: %s\n", arg)
100                 }
101         }
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")
106         }
107         arv := &arvados.Client{
108                 Scheme:    "https",
109                 APIHost:   apiHost,
110                 AuthToken: token,
111                 Insecure:  insecure,
112         }
113         ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(time.Minute))
114         defer cancel()
115         var vms arvados.VirtualMachineList
116         err := arv.RequestAndDecodeContext(ctx, &vms, "GET", "arvados/v1/virtual_machines", nil, arvados.ListOptions{
117                 Limit: 2,
118                 Filters: []arvados.Filter{
119                         {"hostname", "=", hostname},
120                 },
121         })
122         if err != nil {
123                 return err
124         }
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)
131         }
132         var user arvados.User
133         err = arv.RequestAndDecodeContext(ctx, &user, "GET", "arvados/v1/users/current", nil, nil)
134         if err != nil {
135                 return err
136         }
137         var links arvados.LinkList
138         err = arv.RequestAndDecodeContext(ctx, &links, "GET", "arvados/v1/links", nil, arvados.ListOptions{
139                 Limit: 1,
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},
146                 },
147         })
148         if err != nil {
149                 return err
150         }
151         if len(links.Items) < 1 || links.Items[0].Properties["username"] != username {
152                 return errors.New("permission denied")
153         }
154         logger.Debugf("permission granted based on link with UUID %s", links.Items[0].UUID)
155         return nil
156 }
157
158 func newLogger(stderr bool) *logrus.Logger {
159         logger := logrus.New()
160         if !stderr {
161                 logger.Out = ioutil.Discard
162         }
163         if hook, err := lSyslog.NewSyslogHook("udp", "localhost:514", syslog.LOG_AUTH|syslog.LOG_INFO, "pam_arvados"); err != nil {
164                 logger.Hooks.Add(hook)
165         }
166         return logger
167 }