16534: Merge branch 'master'
authorTom Clegg <tom@tomclegg.ca>
Thu, 9 Jul 2020 15:09:18 +0000 (11:09 -0400)
committerTom Clegg <tom@tomclegg.ca>
Thu, 9 Jul 2020 15:09:18 +0000 (11:09 -0400)
Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tom@tomclegg.ca>

22 files changed:
build/run-build-packages-one-target.sh
build/run-build-packages.sh
build/run-library.sh
doc/install/install-api-server.html.textile.liquid
doc/install/install-arv-git-httpd.html.textile.liquid
doc/install/install-keep-web.html.textile.liquid
doc/install/install-keepproxy.html.textile.liquid
doc/install/install-webshell.html.textile.liquid
doc/install/install-workbench-app.html.textile.liquid
doc/install/install-workbench2-app.html.textile.liquid
doc/install/install-ws.html.textile.liquid
lib/pam/.gitignore [new file with mode: 0644]
lib/pam/README [new file with mode: 0644]
lib/pam/docker_test.go [new file with mode: 0644]
lib/pam/fpm-info.sh [new file with mode: 0644]
lib/pam/pam-configs-arvados [new file with mode: 0644]
lib/pam/pam_arvados.go [new file with mode: 0644]
lib/pam/pam_c.go [new file with mode: 0644]
lib/pam/testclient.go [new file with mode: 0644]
sdk/R/install_deps.R
sdk/go/arvados/link.go
sdk/go/arvados/virtual_machine.go [new file with mode: 0644]

index 1a845d200a38c53aeaf3f353e279cf7289a01179..f8816dbe4873c3fad3773d47590393d1e62b5550 100755 (executable)
@@ -208,6 +208,8 @@ if test -z "$packages" ; then
         keepstore
         keep-web
         libarvados-perl
+        libpam-arvados
+        libpam-arvados-go
         python-arvados-fuse
         python-arvados-python-client
         python-arvados-cwl-runner"
index 862b93e6e4667ad561c478e13b0f6f53ee2d012f..5aa0b7e6f8e363642cf3aebfa6bff44d28926d2d 100755 (executable)
@@ -3,8 +3,8 @@
 #
 # SPDX-License-Identifier: AGPL-3.0
 
-. `dirname "$(readlink -f "$0")"`/run-library.sh
-. `dirname "$(readlink -f "$0")"`/libcloud-pin.sh
+. `dirname "$(readlink -f "$0")"`/run-library.sh || exit 1
+. `dirname "$(readlink -f "$0")"`/libcloud-pin.sh || exit 1
 
 read -rd "\000" helpmessage <<EOF
 $(basename $0): Build Arvados packages
@@ -223,7 +223,7 @@ if [[ -z "$ONLY_BUILD" ]] || [[ "libarvados-perl" = "$ONLY_BUILD" ]] ; then
 
     perl Makefile.PL INSTALL_BASE=install >"$STDOUT_IF_DEBUG" && \
         make install INSTALLDIRS=perl >"$STDOUT_IF_DEBUG" && \
-        fpm_build install/lib/=/usr/share libarvados-perl \
+        fpm_build "$WORKSPACE/sdk/perl" install/lib/=/usr/share libarvados-perl \
         dir "$(version_from_git)" install/man/=/usr/share/man \
         "$WORKSPACE/apache-2.0.txt=/usr/share/doc/libarvados-perl/apache-2.0.txt" && \
         mv --no-clobber libarvados-perl*.$FORMAT "$WORKSPACE/packages/$TARGET/"
@@ -271,7 +271,7 @@ debug_echo -e "\nPython packages\n"
       cd "$SRC_BUILD_DIR"
       PKG_VERSION=$(version_from_git)
       cd $WORKSPACE/packages/$TARGET
-      fpm_build $SRC_BUILD_DIR/=/usr/local/arvados/src arvados-src 'dir' "$PKG_VERSION" "--exclude=usr/local/arvados/src/.git" "--url=https://arvados.org" "--license=GNU Affero General Public License, version 3.0" "--description=The Arvados source code" "--architecture=all"
+      fpm_build "$WORKSPACE" $SRC_BUILD_DIR/=/usr/local/arvados/src arvados-src 'dir' "$PKG_VERSION" "--exclude=usr/local/arvados/src/.git" "--url=https://arvados.org" "--license=GNU Affero General Public License, version 3.0" "--description=The Arvados source code" "--architecture=all"
 
       rm -rf "$SRC_BUILD_DIR"
     fi
@@ -318,6 +318,8 @@ package_go_binary tools/keep-rsync keep-rsync \
     "Copy all data from one set of Keep servers to another"
 package_go_binary tools/keep-exercise keep-exercise \
     "Performance testing tool for Arvados Keep"
+package_go_so lib/pam pam_arvados.so libpam-arvados-go \
+    "Arvados PAM authentication module"
 
 # The Python SDK - Should be built first because it's needed by others
 fpm_build_virtualenv "arvados-python-client" "sdk/python"
index f8e5129daeb0ce63aba2230c04214ca252bba476..3e6c9f85841d55be0e7d9794c4e86a693e5500c3 100755 (executable)
@@ -146,7 +146,7 @@ calculate_go_package_version() {
   __returnvar="$version"
 }
 
-# Usage: package_go_binary services/foo arvados-foo "Compute foo to arbitrary precision"
+# Usage: package_go_binary services/foo arvados-foo "Compute foo to arbitrary precision" [apache-2.0.txt]
 package_go_binary() {
     local src_path="$1"; shift
     local prog="$1"; shift
@@ -185,7 +185,37 @@ package_go_binary() {
     fi
     switches+=("$WORKSPACE/${license_file}=/usr/share/doc/$prog/${license_file}")
 
-    fpm_build "$GOPATH/bin/${basename}=/usr/bin/${prog}" "${prog}" dir "${go_package_version}" "--url=https://arvados.org" "--license=GNU Affero General Public License, version 3.0" "--description=${description}" "${switches[@]}"
+    fpm_build "${WORKSPACE}/${src_path}" "$GOPATH/bin/${basename}=/usr/bin/${prog}" "${prog}" dir "${go_package_version}" "--url=https://arvados.org" "--license=GNU Affero General Public License, version 3.0" "--description=${description}" "${switches[@]}"
+}
+
+# Usage: package_go_so lib/foo arvados_foo.so arvados-foo "Arvados foo library"
+package_go_so() {
+    local src_path="$1"; shift
+    local sofile="$1"; shift
+    local pkg="$1"; shift
+    local description="$1"; shift
+
+    debug_echo "package_go_so $src_path as $pkg"
+
+    calculate_go_package_version go_package_version $src_path
+    cd $WORKSPACE/packages/$TARGET
+    test_package_presence $pkg $go_package_version go || return 1
+    cd $WORKSPACE/$src_path
+    go build -buildmode=c-shared -o ${GOPATH}/bin/${sofile}
+    cd $WORKSPACE/packages/$TARGET
+    local -a fpmargs=(
+        "--url=https://arvados.org"
+        "--license=Apache License, Version 2.0"
+        "--description=${description}"
+        "$WORKSPACE/apache-2.0.txt=/usr/share/doc/$pkg/apache-2.0.txt"
+    )
+    if [[ -e "$WORKSPACE/$src_path/pam-configs-arvados" ]]; then
+        fpmargs+=("$WORKSPACE/$src_path/pam-configs-arvados=/usr/share/pam-configs/arvados-go")
+    fi
+    if [[ -e "$WORKSPACE/$src_path/README" ]]; then
+        fpmargs+=("$WORKSPACE/$src_path/README=/usr/share/doc/$pkg/README")
+    fi
+    fpm_build "${WORKSPACE}/${src_path}" "$GOPATH/bin/${sofile}=/usr/lib/${sofile}" "${pkg}" dir "${go_package_version}" "${fpmargs[@]}"
 }
 
 default_iteration() {
@@ -417,7 +447,7 @@ handle_rails_package() {
     for exclude in ${exclude_list[@]}; do
         switches+=(-x "$exclude_root/$exclude")
     done
-    fpm_build "${pos_args[@]}" "${switches[@]}" \
+    fpm_build "${srcdir}" "${pos_args[@]}" "${switches[@]}" \
               -x "$exclude_root/vendor/cache-*" \
               -x "$exclude_root/vendor/bundle" "$@" "$license_arg"
     rm -rf "$scripts_dir"
@@ -736,6 +766,9 @@ fpm_build_virtualenv () {
 
 # Build packages for everything
 fpm_build () {
+  # Source dir where fpm-info.sh (if any) will be found.
+  SRC_DIR=$1
+  shift
   # The package source.  Depending on the source type, this can be a
   # path, or the name of the package in an upstream repository (e.g.,
   # pip).
@@ -812,17 +845,15 @@ fpm_build () {
   declare -a build_depends=()
   declare -a fpm_depends=()
   declare -a fpm_exclude=()
-  declare -a fpm_dirs=(
-      # source dir part of 'dir' package ("/source=/dest" => "/source"):
-      "${PACKAGE%%=/*}")
-  for pkgdir in "${fpm_dirs[@]}"; do
-      fpminfo="$pkgdir/fpm-info.sh"
-      if [[ -e "$fpminfo" ]]; then
-          debug_echo "Loading fpm overrides from $fpminfo"
-          source "$fpminfo"
-          break
-      fi
-  done
+  if [[ ! -d "$SRC_DIR" ]]; then
+      echo >&2 "BUG: looking in wrong dir for fpm-info.sh: $pkgdir"
+      exit 1
+  fi
+  fpminfo="${SRC_DIR}/fpm-info.sh"
+  if [[ -e "$fpminfo" ]]; then
+      debug_echo "Loading fpm overrides from $fpminfo"
+      source "$fpminfo"
+  fi
   for pkg in "${build_depends[@]}"; do
       if [[ $TARGET =~ debian|ubuntu ]]; then
           pkg_deb=$(ls "$WORKSPACE/packages/$TARGET/$pkg_"*.deb | sort -rg | awk 'NR==1')
index e64c3826694f257b5efe688059a39bdf72966021..b8442eb0603dfd5279572c3ec1bc28e7b5bc4e47 100644 (file)
@@ -142,10 +142,9 @@ server {
   # This configures the public https port that clients will actually connect to,
   # the request is reverse proxied to the upstream 'controller'
 
-  listen       *:443 ssl;
-  server_name  <span class="userinput">xxxxx.example.com</span>;
+  listen       443 ssl;
+  server_name  <span class="userinput">ClusterID.example.com</span>;
 
-  ssl on;
   ssl_certificate     <span class="userinput">/YOUR/PATH/TO/cert.pem</span>;
   ssl_certificate_key <span class="userinput">/YOUR/PATH/TO/cert.key</span>;
 
index d501f46b7a02bed09d4d3908716ed7cc442af78f..3d70fc4de9497e8bb20b48cec6ecdfa8f62ef2ca 100644 (file)
@@ -224,12 +224,11 @@ Use a text editor to create a new file @/etc/nginx/conf.d/arvados-git.conf@ with
   server                  127.0.0.1:<span class="userinput">9001</span>;
 }
 server {
-  listen                  *:443 ssl;
+  listen                  443 ssl;
   server_name             git.<span class="userinput">ClusterID.example.com</span>;
   proxy_connect_timeout   90s;
   proxy_read_timeout      300s;
 
-  ssl on;
   ssl_certificate         <span class="userinput">/YOUR/PATH/TO/cert.pem</span>;
   ssl_certificate_key     <span class="userinput">/YOUR/PATH/TO/cert.key</span>;
 
index 0dfcac37e001a9f81b9fa9c4cd6643eeba2c09c5..b31827bf70ed6c0a062cef1321cce68c393caaba 100644 (file)
@@ -121,7 +121,7 @@ upstream keep-web {
 }
 
 server {
-  listen                *:443 ssl;
+  listen                443 ssl;
   server_name           <span class="userinput">download.ClusterID.example.com</span>
                         <span class="userinput">collections.ClusterID.example.com</span>
                         <span class="userinput">*.collections.ClusterID.example.com</span>
index 0839c0e521bd942df9ca4d7f78678bbee805c263..ae6bd3989c340cbc64bb67932d9c1c3d8a8121e9 100644 (file)
@@ -58,7 +58,7 @@ Use a text editor to create a new file @/etc/nginx/conf.d/keepproxy.conf@ with t
 }
 
 server {
-  listen                  *:443 ssl;
+  listen                  443 ssl;
   server_name             <span class="userinput">keep.ClusterID.example.com</span>;
 
   proxy_connect_timeout   90s;
@@ -67,7 +67,6 @@ server {
   proxy_http_version      1.1;
   proxy_request_buffering off;
 
-  ssl on;
   ssl_certificate     <span class="userinput">/YOUR/PATH/TO/cert.pem</span>;
   ssl_certificate_key <span class="userinput">/YOUR/PATH/TO/cert.key</span>;
 
index 4040fcf54f833f7ad5ed2bbce01df6cbbb9a1846..ae6a8d2109c686a3d6769e515e75d4376c1e8bee 100644 (file)
@@ -99,7 +99,7 @@ Note that the location line in the nginx config matches your shell node hostname
 
 For additional shell nodes with @shell-in-a-box@, add @location@ and @upstream@ sections as needed.
 
-{% assign arvados_component = 'shellinabox libpam-arvados' %}
+{% assign arvados_component = 'shellinabox libpam-arvados-go' %}
 
 {% include 'install_packages' %}
 
@@ -149,9 +149,8 @@ h2(#config-pam). Configure pam
 Use a text editor to create a new file @/etc/pam.d/shellinabox@ with the following configuration. Options that need attention are marked in <span class="userinput">red</span>.
 
 <notextile><pre>
-# This example is a stock debian "login" file with libpam_arvados
-# replacing pam_unix, and the "noprompt" option in use. It can be
-# installed as /etc/pam.d/shellinabox .
+# This example is a stock debian "login" file with pam_arvados
+# replacing pam_unix. It can be installed as /etc/pam.d/shellinabox .
 
 auth       optional   pam_faildelay.so  delay=3000000
 auth [success=ok new_authtok_reqd=ok ignore=ignore user_unknown=bad default=die] pam_securetty.so
@@ -160,7 +159,7 @@ session [success=ok ignore=ignore module_unknown=ignore default=bad] pam_selinux
 session       required   pam_env.so readenv=1
 session       required   pam_env.so readenv=1 envfile=/etc/default/locale
 
-auth [success=1 default=ignore] pam_python.so /usr/lib/security/libpam_arvados.py <span class="userinput">ClusterID.example.com</span> <span class="userinput">shell.ClusterID.example.com</span> noprompt
+auth [success=1 default=ignore] /usr/lib/pam_arvados.so <span class="userinput">ClusterID.example.com</span> <span class="userinput">shell.ClusterID.example.com</span>
 auth    requisite            pam_deny.so
 auth    required            pam_permit.so
 
index 3d391724dc1e619590c97cb0bb47c25971e6e05d..7ee8db92f18b94381515e45e67d4d2f8ffb8ea67 100644 (file)
@@ -62,10 +62,9 @@ Use a text editor to create a new file @/etc/nginx/conf.d/arvados-workbench.conf
 }
 
 server {
-  listen       *:443 ssl;
+  listen       443 ssl;
   server_name  workbench.<span class="userinput">ClusterID.example.com</span>;
 
-  ssl on;
   ssl_certificate     <span class="userinput">/YOUR/PATH/TO/cert.pem</span>;
   ssl_certificate_key <span class="userinput">/YOUR/PATH/TO/cert.key</span>;
 
index b59799c43fef0ef45915e4deef8200bfe85ff096..f3a320b10251745f64a8d7eece1e36fb73628e6a 100644 (file)
@@ -47,10 +47,9 @@ Use a text editor to create a new file @/etc/nginx/conf.d/arvados-workbench2.con
 }
 
 server {
-  listen       *:443 ssl;
+  listen       443 ssl;
   server_name  workbench2.<span class="userinput">ClusterID.example.com</span>;
 
-  ssl on;
   ssl_certificate     <span class="userinput">/YOUR/PATH/TO/cert.pem</span>;
   ssl_certificate_key <span class="userinput">/YOUR/PATH/TO/cert.key</span>;
 
index 11862a6ae6767c73ab935e8e93c4134991026789..2b982504f2e705e4334984d35f48aa4ebf1fa05f 100644 (file)
@@ -43,7 +43,7 @@ upstream arvados-ws {
 }
 
 server {
-  listen                *:443 ssl;
+  listen                443 ssl;
   server_name           ws.<span class="userinput">ClusterID.example.com</span>;
 
   proxy_connect_timeout 90s;
diff --git a/lib/pam/.gitignore b/lib/pam/.gitignore
new file mode 100644 (file)
index 0000000..8d44d63
--- /dev/null
@@ -0,0 +1,2 @@
+pam_arvados.h
+pam_arvados.so
diff --git a/lib/pam/README b/lib/pam/README
new file mode 100644 (file)
index 0000000..8c5e10e
--- /dev/null
@@ -0,0 +1,18 @@
+For configuration advice, please refer to https://doc.arvados.org/install/install-webshell.html
+
+Usage (in pam config):
+
+    pam_arvados.so arvados_api_host my_vm_hostname ["insecure"] ["debug"]
+
+pam_arvados.so passes authentication if (according to
+arvados_api_host) the supplied PAM token belongs to an Arvados user
+who is allowed to log in to my_vm_host_name with the supplied PAM
+username.
+
+If my_vm_hostname is omitted or "-", the current hostname is used.
+
+"insecure" -- continue even if the TLS certificate presented by
+arvados_api_host fails verification.
+
+"debug" -- enable debug-level log messages in syslog and (when not in
+"silent" mode) on the calling application's stderr.
diff --git a/lib/pam/docker_test.go b/lib/pam/docker_test.go
new file mode 100644 (file)
index 0000000..fa16b31
--- /dev/null
@@ -0,0 +1,173 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: Apache-2.0
+
+package main
+
+import (
+       "bytes"
+       "crypto/tls"
+       "fmt"
+       "io/ioutil"
+       "net"
+       "net/http"
+       "net/http/httputil"
+       "net/url"
+       "os"
+       "os/exec"
+       "strings"
+       "testing"
+
+       "git.arvados.org/arvados.git/sdk/go/arvadostest"
+       "gopkg.in/check.v1"
+)
+
+type DockerSuite struct {
+       tmpdir   string
+       hostip   string
+       proxyln  net.Listener
+       proxysrv *http.Server
+}
+
+var _ = check.Suite(&DockerSuite{})
+
+func Test(t *testing.T) { check.TestingT(t) }
+
+func (s *DockerSuite) SetUpSuite(c *check.C) {
+       if testing.Short() {
+               c.Skip("skipping docker tests in short mode")
+       } else if _, err := exec.Command("docker", "info").CombinedOutput(); err != nil {
+               c.Skip("skipping docker tests because docker is not available")
+       }
+
+       s.tmpdir = c.MkDir()
+
+       // The integration-testing controller listens on the loopback
+       // interface, so it won't be reachable directly from the
+       // docker container -- so here we run a proxy on 0.0.0.0 for
+       // the duration of the test.
+       hostips, err := exec.Command("hostname", "-I").Output()
+       c.Assert(err, check.IsNil)
+       s.hostip = strings.Split(strings.Trim(string(hostips), "\n"), " ")[0]
+       ln, err := net.Listen("tcp", s.hostip+":0")
+       c.Assert(err, check.IsNil)
+       s.proxyln = ln
+       proxy := httputil.NewSingleHostReverseProxy(&url.URL{Scheme: "https", Host: os.Getenv("ARVADOS_API_HOST")})
+       proxy.Transport = &http.Transport{
+               TLSClientConfig: &tls.Config{
+                       InsecureSkipVerify: true,
+               },
+       }
+       s.proxysrv = &http.Server{Handler: proxy}
+       go s.proxysrv.ServeTLS(ln, "../../services/api/tmp/self-signed.pem", "../../services/api/tmp/self-signed.key")
+
+       // Build a pam module to install & configure in the docker
+       // container.
+       cmd := exec.Command("go", "build", "-buildmode=c-shared", "-o", s.tmpdir+"/pam_arvados.so")
+       cmd.Stdout = os.Stdout
+       cmd.Stderr = os.Stderr
+       err = cmd.Run()
+       c.Assert(err, check.IsNil)
+
+       // Build the testclient program that will (from inside the
+       // docker container) configure the system to use the above PAM
+       // config, and then try authentication.
+       cmd = exec.Command("go", "build", "-o", s.tmpdir+"/testclient", "./testclient.go")
+       cmd.Stdout = os.Stdout
+       cmd.Stderr = os.Stderr
+       err = cmd.Run()
+       c.Assert(err, check.IsNil)
+}
+
+func (s *DockerSuite) TearDownSuite(c *check.C) {
+       if s.proxysrv != nil {
+               s.proxysrv.Close()
+       }
+       if s.proxyln != nil {
+               s.proxyln.Close()
+       }
+}
+
+func (s *DockerSuite) SetUpTest(c *check.C) {
+       // Write a PAM config file that uses our proxy as
+       // ARVADOS_API_HOST.
+       proxyhost := s.proxyln.Addr().String()
+       confdata := fmt.Sprintf(`Name: Arvados authentication
+Default: yes
+Priority: 256
+Auth-Type: Primary
+Auth:
+       [success=end default=ignore]    /usr/lib/pam_arvados.so %s testvm2.shell insecure
+Auth-Initial:
+       [success=end default=ignore]    /usr/lib/pam_arvados.so %s testvm2.shell insecure
+`, proxyhost, proxyhost)
+       err := ioutil.WriteFile(s.tmpdir+"/conffile", []byte(confdata), 0755)
+       c.Assert(err, check.IsNil)
+}
+
+func (s *DockerSuite) runTestClient(c *check.C, args ...string) (stdout, stderr *bytes.Buffer, err error) {
+
+       cmd := exec.Command("docker", append([]string{
+               "run", "--rm",
+               "--hostname", "testvm2.shell",
+               "--add-host", "zzzzz.arvadosapi.com:" + s.hostip,
+               "-v", s.tmpdir + "/pam_arvados.so:/usr/lib/pam_arvados.so:ro",
+               "-v", s.tmpdir + "/conffile:/usr/share/pam-configs/arvados:ro",
+               "-v", s.tmpdir + "/testclient:/testclient:ro",
+               "debian:buster",
+               "/testclient"}, args...)...)
+       stdout = &bytes.Buffer{}
+       stderr = &bytes.Buffer{}
+       cmd.Stdout = stdout
+       cmd.Stderr = stderr
+       err = cmd.Run()
+       return
+}
+
+func (s *DockerSuite) TestSuccess(c *check.C) {
+       stdout, stderr, err := s.runTestClient(c, "try", "active", arvadostest.ActiveTokenV2)
+       c.Check(err, check.IsNil)
+       c.Logf("%s", stderr.String())
+       c.Check(stdout.String(), check.Equals, "")
+       c.Check(stderr.String(), check.Matches, `(?ms).*authentication succeeded.*`)
+}
+
+func (s *DockerSuite) TestFailure(c *check.C) {
+       for _, trial := range []struct {
+               label    string
+               username string
+               token    string
+       }{
+               {"bad token", "active", arvadostest.ActiveTokenV2 + "badtoken"},
+               {"empty token", "active", ""},
+               {"empty username", "", arvadostest.ActiveTokenV2},
+               {"wrong username", "wrongusername", arvadostest.ActiveTokenV2},
+       } {
+               c.Logf("trial: %s", trial.label)
+               stdout, stderr, err := s.runTestClient(c, "try", trial.username, trial.token)
+               c.Logf("%s", stderr.String())
+               c.Check(err, check.NotNil)
+               c.Check(stdout.String(), check.Equals, "")
+               c.Check(stderr.String(), check.Matches, `(?ms).*authentication failed.*`)
+       }
+}
+
+func (s *DockerSuite) TestDefaultHostname(c *check.C) {
+       confdata := fmt.Sprintf(`Name: Arvados authentication
+Default: yes
+Priority: 256
+Auth-Type: Primary
+Auth:
+       [success=end default=ignore]    /usr/lib/pam_arvados.so %s - insecure debug
+Auth-Initial:
+       [success=end default=ignore]    /usr/lib/pam_arvados.so %s - insecure debug
+`, s.proxyln.Addr().String(), s.proxyln.Addr().String())
+       err := ioutil.WriteFile(s.tmpdir+"/conffile", []byte(confdata), 0755)
+       c.Assert(err, check.IsNil)
+
+       stdout, stderr, err := s.runTestClient(c, "try", "active", arvadostest.ActiveTokenV2)
+       c.Check(err, check.IsNil)
+       c.Logf("%s", stderr.String())
+       c.Check(stdout.String(), check.Equals, "")
+       c.Check(stderr.String(), check.Matches, `(?ms).*authentication succeeded.*`)
+}
diff --git a/lib/pam/fpm-info.sh b/lib/pam/fpm-info.sh
new file mode 100644 (file)
index 0000000..3366b8e
--- /dev/null
@@ -0,0 +1,5 @@
+# Copyright (C) The Arvados Authors. All rights reserved.
+#
+# SPDX-License-Identifier: Apache-2.0
+
+fpm_depends+=(ca-certificates)
diff --git a/lib/pam/pam-configs-arvados b/lib/pam/pam-configs-arvados
new file mode 100644 (file)
index 0000000..37ed4b8
--- /dev/null
@@ -0,0 +1,19 @@
+# Copyright (C) The Arvados Authors. All rights reserved.
+#
+# SPDX-License-Identifier: Apache-2.0
+
+# This file is packaged as /usr/share/pam-configs/arvados-go; see build/run-library.sh
+
+# 1. Run `pam-auth-update` and choose Arvados authentication
+# 2. In /etc/pam.d/common-auth, change "api.example" to your ARVADOS_API_HOST
+# 3. In /etc/pam.d/common-auth, change "shell.example" to this host's hostname
+#    (as it appears in the Arvados virtual_machines list)
+
+Name: Arvados authentication
+Default: yes
+Priority: 256
+Auth-Type: Primary
+Auth:
+       [success=end default=ignore]    /usr/lib/pam_arvados.so api.example shell.example
+Auth-Initial:
+       [success=end default=ignore]    /usr/lib/pam_arvados.so api.example shell.example
diff --git a/lib/pam/pam_arvados.go b/lib/pam/pam_arvados.go
new file mode 100644 (file)
index 0000000..34b9080
--- /dev/null
@@ -0,0 +1,185 @@
+// 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 <security/pam_ext.h>
+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("unkown 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
+}
diff --git a/lib/pam/pam_c.go b/lib/pam/pam_c.go
new file mode 100644 (file)
index 0000000..4bf975b
--- /dev/null
@@ -0,0 +1,24 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: Apache-2.0
+
+package main
+
+/*
+#cgo LDFLAGS: -lpam -fPIC
+#include <security/pam_ext.h>
+char *stringindex(char** a, int i) { return a[i]; }
+const char *get_user(pam_handle_t *pamh) {
+  const char *user;
+  if (pam_get_item(pamh, PAM_USER, (const void**)&user) != PAM_SUCCESS)
+    return NULL;
+  return user;
+}
+const char *get_authtoken(pam_handle_t *pamh) {
+  const char *token;
+  if (pam_get_authtok(pamh, PAM_AUTHTOK, &token, NULL) != PAM_SUCCESS)
+    return NULL;
+  return token;
+}
+*/
+import "C"
diff --git a/lib/pam/testclient.go b/lib/pam/testclient.go
new file mode 100644 (file)
index 0000000..3e92cac
--- /dev/null
@@ -0,0 +1,83 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: Apache-2.0
+
+// +build never
+
+// This file is compiled by docker_test.go to build a test client.
+// It's not part of the pam module itself.
+
+package main
+
+import (
+       "fmt"
+       "os"
+       "os/exec"
+
+       "github.com/msteinert/pam"
+       "github.com/sirupsen/logrus"
+)
+
+func main() {
+       if len(os.Args) != 4 || os.Args[1] != "try" {
+               logrus.Print("usage: testclient try 'username' 'password'")
+               os.Exit(1)
+       }
+       username := os.Args[2]
+       password := os.Args[3]
+
+       // Configure PAM to use arvados token auth by default.
+       cmd := exec.Command("pam-auth-update", "--force", "arvados", "--remove", "unix")
+       cmd.Env = append([]string{"DEBIAN_FRONTEND=noninteractive"}, os.Environ()...)
+       cmd.Stdin = nil
+       cmd.Stdout = os.Stdout
+       cmd.Stderr = os.Stderr
+       err := cmd.Run()
+       if err != nil {
+               logrus.WithError(err).Error("pam-auth-update failed")
+               os.Exit(1)
+       }
+
+       // Check that pam-auth-update actually added arvados config.
+       cmd = exec.Command("grep", "-Hn", "arvados", "/etc/pam.d/common-auth")
+       cmd.Stdout = os.Stderr
+       cmd.Stderr = os.Stderr
+       err = cmd.Run()
+       if err != nil {
+               panic(err)
+       }
+
+       logrus.Debugf("starting pam: username=%q password=%q", username, password)
+
+       sentPassword := false
+       errorMessage := ""
+       tx, err := pam.StartFunc("default", username, func(style pam.Style, message string) (string, error) {
+               logrus.Debugf("pam conversation: style=%v message=%q", style, message)
+               switch style {
+               case pam.ErrorMsg:
+                       logrus.WithField("Message", message).Info("pam.ErrorMsg")
+                       errorMessage = message
+                       return "", nil
+               case pam.TextInfo:
+                       logrus.WithField("Message", message).Info("pam.TextInfo")
+                       errorMessage = message
+                       return "", nil
+               case pam.PromptEchoOn, pam.PromptEchoOff:
+                       sentPassword = true
+                       return password, nil
+               default:
+                       return "", fmt.Errorf("unrecognized message style %d", style)
+               }
+       })
+       if err != nil {
+               logrus.WithError(err).Print("StartFunc failed")
+               os.Exit(1)
+       }
+       err = tx.Authenticate(pam.DisallowNullAuthtok)
+       if err != nil {
+               err = fmt.Errorf("PAM: %s (message = %q)", err, errorMessage)
+               logrus.WithError(err).Print("authentication failed")
+               os.Exit(1)
+       }
+       logrus.Print("authentication succeeded")
+}
index 593129bb3ceeb18bbc6cb2520529fd5067b823b1..6c33f97913f83aaeaebf392fec740ba9f9d0d98a 100644 (file)
@@ -15,5 +15,11 @@ if (!requireNamespace("knitr")) {
 if (!requireNamespace("markdown")) {
   install.packages("markdown")
 }
+if (!requireNamespace("XML")) {
+  # XML 3.99-0.4 depends on R >= 4.0.0, but we run tests on debian
+  # stable (10) with R 3.5.2 so we install an older version from
+  # source.
+  install.packages("https://cran.r-project.org/src/contrib/Archive/XML/XML_3.99-0.3.tar.gz", repos=NULL, type="source")
+}
 
 devtools::install_dev_deps()
index fbd699f30653035ff8c23ad1f62223c5ca54adc9..fdddfc537d8ee3b1dca86853232dc7017851969b 100644 (file)
@@ -6,14 +6,15 @@ package arvados
 
 // Link is an arvados#link record
 type Link struct {
-       UUID      string `json:"uuid,omiempty"`
-       OwnerUUID string `json:"owner_uuid"`
-       Name      string `json:"name"`
-       LinkClass string `json:"link_class"`
-       HeadUUID  string `json:"head_uuid"`
-       HeadKind  string `json:"head_kind"`
-       TailUUID  string `json:"tail_uuid"`
-       TailKind  string `json:"tail_kind"`
+       UUID       string                 `json:"uuid,omiempty"`
+       OwnerUUID  string                 `json:"owner_uuid"`
+       Name       string                 `json:"name"`
+       LinkClass  string                 `json:"link_class"`
+       HeadUUID   string                 `json:"head_uuid"`
+       HeadKind   string                 `json:"head_kind"`
+       TailUUID   string                 `json:"tail_uuid"`
+       TailKind   string                 `json:"tail_kind"`
+       Properties map[string]interface{} `json:"properties"`
 }
 
 // UserList is an arvados#userList resource.
diff --git a/sdk/go/arvados/virtual_machine.go b/sdk/go/arvados/virtual_machine.go
new file mode 100644 (file)
index 0000000..1506ede
--- /dev/null
@@ -0,0 +1,25 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: Apache-2.0
+
+package arvados
+
+import "time"
+
+// VirtualMachine is an arvados#virtualMachine resource.
+type VirtualMachine struct {
+       UUID               string     `json:"uuid"`
+       OwnerUUID          string     `json:"owner_uuid"`
+       Hostname           string     `json:"hostname"`
+       CreatedAt          *time.Time `json:"created_at"`
+       ModifiedAt         *time.Time `json:"modified_at"`
+       ModifiedByUserUUID string     `json:"modified_by_user_uuid"`
+}
+
+// VirtualMachineList is an arvados#virtualMachineList resource.
+type VirtualMachineList struct {
+       Items          []VirtualMachine `json:"items"`
+       ItemsAvailable int              `json:"items_available"`
+       Offset         int              `json:"offset"`
+       Limit          int              `json:"limit"`
+}