Merge branch '16471-install-using-salt' into master
authorJavier Bértoli <jbertoli@curii.com>
Wed, 21 Oct 2020 15:47:09 +0000 (12:47 -0300)
committerJavier Bértoli <jbertoli@curii.com>
Wed, 21 Oct 2020 15:49:04 +0000 (12:49 -0300)
closes #16471
Arvados-DCO-1.1-Signed-off-by: Javier Bértoli <jbertoli@curii.com>

75 files changed:
.licenseignore
CONTRIBUTING.md
build/build-dev-docker-jobs-image.sh
build/package-build-dockerfiles/Makefile
build/package-test-dockerfiles/Makefile
build/rails-package-scripts/postinst.sh
build/run-build-docker-jobs-image.sh
build/run-library.sh
build/version-at-commit.sh
doc/_config.yml
doc/admin/upgrading.html.textile.liquid
doc/api/keep-s3.html.textile.liquid [new file with mode: 0644]
doc/api/keep-web-urls.html.textile.liquid [new file with mode: 0644]
doc/api/keep-webdav.html.textile.liquid [new file with mode: 0644]
doc/api/methods/groups.html.textile.liquid
doc/install/new_cluster_checklist_AWS.xlsx
doc/install/new_cluster_checklist_Azure.xlsx
doc/install/new_cluster_checklist_slurm.xlsx
lib/cmd/cmd.go
lib/controller/api/routable.go
lib/controller/auth_test.go [new file with mode: 0644]
lib/controller/federation.go
lib/controller/federation/conn.go
lib/controller/handler.go
lib/controller/integration_test.go
lib/controller/localdb/login_oidc.go
lib/controller/localdb/login_oidc_test.go
sdk/R/R/Collection.R
sdk/R/README.Rmd
sdk/cli/arvados-cli.gemspec
sdk/cwl/arvados_version.py
sdk/cwl/test_with_arvbox.sh
sdk/cwl/tests/federation/arvboxcwl/fed-config.cwl
sdk/go/arvadostest/oidc_provider.go [new file with mode: 0644]
sdk/go/auth/auth.go
sdk/go/blockdigest/blockdigest.go
sdk/go/blockdigest/testing.go
sdk/go/keepclient/support.go
sdk/python/arvados/api.py
sdk/python/arvados_version.py
sdk/python/tests/fed-migrate/README
sdk/ruby/arvados.gemspec
services/api/app/controllers/application_controller.rb
services/api/app/controllers/arvados/v1/collections_controller.rb
services/api/app/controllers/arvados/v1/container_requests_controller.rb
services/api/app/controllers/arvados/v1/groups_controller.rb
services/api/app/controllers/arvados/v1/jobs_controller.rb
services/api/app/controllers/arvados/v1/users_controller.rb
services/api/app/middlewares/arvados_api_token.rb
services/api/app/models/api_client_authorization.rb
services/api/app/models/user.rb
services/api/test/functional/arvados/v1/groups_controller_test.rb
services/api/test/functional/arvados/v1/schema_controller_test.rb
services/api/test/integration/remote_user_test.rb
services/api/test/unit/user_test.rb
services/dockercleaner/arvados_version.py
services/fuse/arvados_version.py
services/keep-web/doc.go
services/login-sync/arvados-login-sync.gemspec
services/ws/doc.go
tools/arvbox/bin/arvbox
tools/arvbox/lib/arvbox/docker/cluster-config.sh
tools/arvbox/lib/arvbox/docker/common.sh
tools/arvbox/lib/arvbox/docker/runsu.sh
tools/arvbox/lib/arvbox/docker/service/api/run-service
tools/arvbox/lib/arvbox/docker/service/doc/run-service
tools/arvbox/lib/arvbox/docker/service/gitolite/run-service
tools/arvbox/lib/arvbox/docker/service/keepproxy/run-service
tools/arvbox/lib/arvbox/docker/service/nginx/run
tools/arvbox/lib/arvbox/docker/service/vm/run-service
tools/arvbox/lib/arvbox/docker/service/workbench/run-service
tools/arvbox/lib/arvbox/docker/service/workbench2/run-service
tools/crunchstat-summary/arvados_version.py
tools/sync-groups/sync-groups.go
tools/sync-groups/sync-groups_test.go

index 490c8447c0b48b75d1e8ba6b8d95055a49b3e179..7ebc82667ce80565575029ff2df12fa44703297e 100644 (file)
@@ -85,3 +85,4 @@ go.sum
 sdk/python/tests/fed-migrate/CWLFile
 sdk/python/tests/fed-migrate/*.cwl
 sdk/python/tests/fed-migrate/*.cwlex
+doc/install/*.xlsx
index 459d7277a52134159d35417a1892d491d4ae9e4e..39483ce62d879d5e7c8ba645315b0041f5271bd1 100644 (file)
@@ -31,7 +31,7 @@ https://github.com/arvados/arvados .
 
 Visit [Hacking Arvados](https://dev.arvados.org/projects/arvados/wiki/Hacking) for
 detailed information about setting up an Arvados development
-environment, development process, coding standards, and notes about specific components.
+environment, development process, [coding standards](https://dev.arvados.org/projects/arvados/wiki/Coding_Standards), and notes about specific components.
 
 If you wish to build the Arvados documentation from a local git clone, see
 [doc/README.textile](doc/README.textile) for instructions.
index 7da8089837df30872ec0e00761a33cd5829d27cb..3fb1da7990929e859b9b7da21521702f13b916b0 100755 (executable)
@@ -71,6 +71,8 @@ fi
 
 calculate_python_sdk_cwl_package_versions
 
+cwl_runner_version=$(echo -n $cwl_runner_version | sed s/~dev/.dev/g | sed s/~rc/rc/g)
+
 set -x
 docker build --no-cache --build-arg sdk=$sdk --build-arg runner=$runner --build-arg salad=$salad --build-arg cwltool=$cwltool --build-arg pythoncmd=$py --build-arg pipcmd=$pipcmd -f "$WORKSPACE/sdk/dev-jobs.dockerfile" -t arvados/jobs:$cwl_runner_version "$WORKSPACE/sdk"
 echo arv-keepdocker arvados/jobs $cwl_runner_version
index 818f2575254f91ab81cacdb04f9db055ad68e1b8..e116cb7fc3ffb2688abcf5a5c6fe10d6e32d4718 100644 (file)
@@ -6,24 +6,24 @@ all: centos7/generated debian9/generated debian10/generated ubuntu1604/generated
 
 centos7/generated: common-generated-all
        test -d centos7/generated || mkdir centos7/generated
-       cp -rlt centos7/generated common-generated/*
+       cp -f -rlt centos7/generated common-generated/*
 
 debian9/generated: common-generated-all
        test -d debian9/generated || mkdir debian9/generated
-       cp -rlt debian9/generated common-generated/*
+       cp -f -rlt debian9/generated common-generated/*
 
 debian10/generated: common-generated-all
        test -d debian10/generated || mkdir debian10/generated
-       cp -rlt debian10/generated common-generated/*
+       cp -f -rlt debian10/generated common-generated/*
 
 
 ubuntu1604/generated: common-generated-all
        test -d ubuntu1604/generated || mkdir ubuntu1604/generated
-       cp -rlt ubuntu1604/generated common-generated/*
+       cp -f -rlt ubuntu1604/generated common-generated/*
 
 ubuntu1804/generated: common-generated-all
        test -d ubuntu1804/generated || mkdir ubuntu1804/generated
-       cp -rlt ubuntu1804/generated common-generated/*
+       cp -f -rlt ubuntu1804/generated common-generated/*
 
 GOTARBALL=go1.13.4.linux-amd64.tar.gz
 NODETARBALL=node-v6.11.2-linux-x64.tar.xz
index 1066750fe551c583edc1059a0fcb750f98799e8b..4bc13b0f34a205db65bc8afbaa384454f39cb8c6 100644 (file)
@@ -6,23 +6,23 @@ all: centos7/generated debian9/generated  debian10/generated ubuntu1604/generate
 
 centos7/generated: common-generated-all
        test -d centos7/generated || mkdir centos7/generated
-       cp -rlt centos7/generated common-generated/*
+       cp -f -rlt centos7/generated common-generated/*
 
 debian9/generated: common-generated-all
        test -d debian9/generated || mkdir debian9/generated
-       cp -rlt debian9/generated common-generated/*
+       cp -f -rlt debian9/generated common-generated/*
 
 debian10/generated: common-generated-all
        test -d debian10/generated || mkdir debian10/generated
-       cp -rlt debian10/generated common-generated/*
+       cp -f -rlt debian10/generated common-generated/*
 
 ubuntu1604/generated: common-generated-all
        test -d ubuntu1604/generated || mkdir ubuntu1604/generated
-       cp -rlt ubuntu1604/generated common-generated/*
+       cp -f -rlt ubuntu1604/generated common-generated/*
 
 ubuntu1804/generated: common-generated-all
        test -d ubuntu1804/generated || mkdir ubuntu1804/generated
-       cp -rlt ubuntu1804/generated common-generated/*
+       cp -f -rlt ubuntu1804/generated common-generated/*
 
 RVMKEY1=mpapis.asc
 RVMKEY2=pkuczynski.asc
index 7ea21848b2cf5d4a047e9e5e7d0131584954552c..357c1006431452385cc3a60c028e396452b1ffc6 100644 (file)
@@ -212,6 +212,8 @@ configure_version() {
   chown "$WWW_OWNER:" $RELEASE_PATH/Gemfile.lock
   chown -R "$WWW_OWNER:" $RELEASE_PATH/tmp || true
   chown -R "$WWW_OWNER:" $SHARED_PATH/log
+  # Make sure postgres doesn't try to use a pager.
+  export PAGER=
   case "$RAILSPKG_DATABASE_LOAD_TASK" in
       db:schema:load) chown "$WWW_OWNER:" $RELEASE_PATH/db/schema.rb ;;
       db:structure:load) chown "$WWW_OWNER:" $RELEASE_PATH/db/structure.sql ;;
index d1fb2ac67054dfdc31ce8a31401747c3a55aefbf..d2c9f7274b3e4a006bd335229fbbbd16c905ac13 100755 (executable)
@@ -149,7 +149,13 @@ else
        python_sdk_version="${ARVADOS_BUILDING_VERSION}-${ARVADOS_BUILDING_ITERATION}"
 fi
 
-cwl_runner_version_orig=$cwl_runner_version
+# What we use to tag the Docker image.  For development and release
+# candidate packages, the OS package has a "~dev" or "~rc" suffix, but
+# Python requires a ".dev" or "rc" suffix.  Arvados-cwl-runner will be
+# expecting the Python-compatible version string when it tries to pull
+# the Docker image, but --build-arg is expecting the OS package
+# version.
+cwl_runner_version_tag=$(echo -n $cwl_runner_version | sed s/~dev/.dev/g | sed s/~rc/rc/g)
 
 if [[ "${cwl_runner_version}" != "${ARVADOS_BUILDING_VERSION}" ]]; then
        cwl_runner_version="${cwl_runner_version}-1"
@@ -162,7 +168,7 @@ docker build $NOCACHE \
        --build-arg python_sdk_version=${python_sdk_version} \
        --build-arg cwl_runner_version=${cwl_runner_version} \
        --build-arg repo_version=${REPO} \
-       -t arvados/jobs:$cwl_runner_version_orig .
+       -t arvados/jobs:$cwl_runner_version_tag .
 
 ECODE=$?
 
@@ -185,18 +191,6 @@ if docker --version |grep " 1\.[0-9]\." ; then
     FORCE=-f
 fi
 
-if ! [[ -z "$version_tag" ]]; then
-    docker tag $FORCE arvados/jobs:$cwl_runner_version_orig arvados/jobs:"$version_tag"
-    ECODE=$?
-
-    if [[ "$ECODE" != "0" ]]; then
-        EXITCODE=$(($EXITCODE + $ECODE))
-    fi
-
-    checkexit $ECODE "docker tag"
-    title "docker tag complete (`timer`)"
-fi
-
 title "uploading images"
 
 timer_reset
@@ -208,11 +202,7 @@ else
         ## 20150526 nico -- *sometimes* dockerhub needs re-login
         ## even though credentials are already in .dockercfg
         docker login -u arvados
-        if ! [[ -z "$version_tag" ]]; then
-            docker_push arvados/jobs:"$version_tag"
-        else
-           docker_push arvados/jobs:$cwl_runner_version_orig
-        fi
+        docker_push arvados/jobs:$cwl_runner_version_tag
         title "upload arvados images finished (`timer`)"
     else
         title "upload arvados images SKIPPED because no --upload option set (`timer`)"
index bd150e6a9b36ae96ed57792f2c7f79c51d21fcae..bd4d1aeb1f65ac00d0594ea15f9ae7bba3f13063 100755 (executable)
@@ -130,9 +130,9 @@ calculate_go_package_version() {
       checkdirs+=("$1")
       shift
   done
-  if grep -qr git.arvados.org/arvados .; then
-      checkdirs+=(sdk/go lib)
-  fi
+  # Even our rails packages (version calculation happens here!) depend on a go component (arvados-server)
+  # Everything depends on the build directory.
+  checkdirs+=(sdk/go lib build)
   local timestamp=0
   for dir in ${checkdirs[@]}; do
       cd "$WORKSPACE"
@@ -253,7 +253,7 @@ rails_package_version() {
     fi
     local version="$(version_from_git)"
     if [ $pkgname = "arvados-api-server" -o $pkgname = "arvados-workbench" ] ; then
-       calculate_go_package_version version cmd/arvados-server "$srcdir"
+        calculate_go_package_version version cmd/arvados-server "$srcdir"
     fi
     echo $version
 }
@@ -525,13 +525,19 @@ fpm_build_virtualenv () {
   fi
 
   # Determine the package version from the generated sdist archive
-  PYTHON_VERSION=${ARVADOS_BUILDING_VERSION:-$(awk '($1 == "Version:"){print $2}' *.egg-info/PKG-INFO)}
+  if [[ -n "$ARVADOS_BUILDING_VERSION" ]] ; then
+      UNFILTERED_PYTHON_VERSION=$ARVADOS_BUILDING_VERSION
+      PYTHON_VERSION=$(echo -n $ARVADOS_BUILDING_VERSION | sed s/~dev/.dev/g | sed s/~rc/rc/g)
+  else
+      PYTHON_VERSION=$(awk '($1 == "Version:"){print $2}' *.egg-info/PKG-INFO)
+      UNFILTERED_PYTHON_VERSION=$(echo -n $PYTHON_VERSION | sed s/\.dev/~dev/g |sed 's/\([0-9]\)rc/\1~rc/g')
+  fi
 
   # See if we actually need to build this package; does it exist already?
   # We can't do this earlier than here, because we need PYTHON_VERSION...
   # This isn't so bad; the sdist call above is pretty quick compared to
   # the invocation of virtualenv and fpm, below.
-  if ! test_package_presence "$PYTHON_PKG" $PYTHON_VERSION $PACKAGE_TYPE $ARVADOS_BUILDING_ITERATION; then
+  if ! test_package_presence "$PYTHON_PKG" $UNFILTERED_PYTHON_VERSION $PACKAGE_TYPE $ARVADOS_BUILDING_ITERATION; then
     return 0
   fi
 
@@ -642,7 +648,7 @@ fpm_build_virtualenv () {
     COMMAND_ARR+=('--verbose' '--log' 'info')
   fi
 
-  COMMAND_ARR+=('-v' "$PYTHON_VERSION")
+  COMMAND_ARR+=('-v' $(echo -n "$PYTHON_VERSION" | sed s/.dev/~dev/g | sed s/rc/~rc/g))
   COMMAND_ARR+=('--iteration' "$ARVADOS_BUILDING_ITERATION")
   COMMAND_ARR+=('-n' "$PYTHON_PKG")
   COMMAND_ARR+=('-C' "build")
index 89684cf2abdb32b8b6b749a22cf03caf2bba5bcf..53687dafec9fbd883c660e753d4800366cf522a4 100755 (executable)
@@ -1,9 +1,12 @@
 #!/bin/bash
+# Copyright (C) The Arvados Authors. All rights reserved.
+#
+# SPDX-License-Identifier: AGPL-3.0
 
 set -e -o pipefail
 commit="$1"
 versionglob="[0-9].[0-9]*.[0-9]*"
-devsuffix=".dev"
+devsuffix="~dev"
 
 # automatically assign version
 #
index ec0bb5cfb254593ceab10f3ef6f53f5ad6000037..d56a95c1e23046b2cb95b443c86981b09927e3f0 100644 (file)
@@ -124,6 +124,9 @@ navbar:
       - api/methods/virtual_machines.html.textile.liquid
       - api/methods/keep_disks.html.textile.liquid
     - Data management:
+      - api/keep-webdav.html.textile.liquid
+      - api/keep-s3.html.textile.liquid
+      - api/keep-web-urls.html.textile.liquid
       - api/methods/collections.html.textile.liquid
       - api/methods/repositories.html.textile.liquid
     - Container engine:
index de700d5598083e28fe2a8c5a333c1bb2b143f7bb..d0a29f598be9bdd2e5187b715b16fe2305241351 100644 (file)
@@ -35,7 +35,7 @@ TODO: extract this information based on git commit messages and generate changel
 <div class="releasenotes">
 </notextile>
 
-h2(#master). development master (as of 2020-09-28)
+h2(#v2_1_0). v2.1.0 (2020-10-13)
 
 "Upgrading from 2.0.0":#v2_0_0
 
diff --git a/doc/api/keep-s3.html.textile.liquid b/doc/api/keep-s3.html.textile.liquid
new file mode 100644 (file)
index 0000000..2cae817
--- /dev/null
@@ -0,0 +1,74 @@
+---
+layout: default
+navsection: api
+navmenu: API Methods
+title: "S3 API"
+
+...
+{% comment %}
+Copyright (C) The Arvados Authors. All rights reserved.
+
+SPDX-License-Identifier: CC-BY-SA-3.0
+{% endcomment %}
+
+The Simple Storage Service (S3) API is a de-facto standard for object storage originally developed by Amazon Web Services.  Arvados supports accessing files in Keep using the S3 API.
+
+S3 is supported by many "cloud native" applications, and client libraries exist in many languages for programmatic access.
+
+h3. Endpoints and Buckets
+
+To access Arvados S3 using an S3 client library, you must tell it to use the URL of the keep-web server (this is @Services.WebDAVDownload.ExternalURL@ in the public configuration) as the custom endpoint.  The keep-web server will decide to treat it as an S3 API request based on the presence of an AWS-format Authorization header.  Requests without an Authorization header, or differently formatted Authorization, will be treated as "WebDAV":keep-webdav.html .
+
+The "bucket name" is an Arvados collection uuid, portable data hash, or project uuid.
+
+The bucket name must be encoded as the first path segment of every request.  This is what the S3 documentation calls "Path-Style Requests".
+
+h3. Supported Operations
+
+h4. ListObjects
+
+Supports the following request query parameters:
+
+* delimiter
+* marker
+* max-keys
+* prefix
+
+h4. GetObject
+
+Supports the @Range@ header.
+
+h4. PutObject
+
+Can be used to create or replace a file in a collection.
+
+An empty PUT with a trailing slash and @Content-Type: application/x-directory@ will create a directory within a collection if Arvados configuration option @Collections.S3FolderObjects@ is true.
+
+Missing parent/intermediate directories within a collection are created automatically.
+
+Cannot be used to create a collection or project.
+
+h4. DeleteObject
+
+Can be used to remove files from a collection.
+
+If used on a directory marker, it will delete the directory only if the directory is empty.
+
+h4. HeadBucket
+
+Can be used to determine if a bucket exists and if client has read access to it.
+
+h4. HeadObject
+
+Can be used to determine if an object exists and if client has read access to it.
+
+h4. GetBucketVersioning
+
+Bucket versioning is presently not supported, so this will always respond that bucket versioning is not enabled.
+
+h3. Authorization mechanisms
+
+Keep-web accepts AWS Signature Version 4 (AWS4-HMAC-SHA256) as well as the older V2 AWS signature.
+
+* If your client uses V4 signatures exclusively: use the Arvados token's UUID part as AccessKey, and its secret part as SecretKey.  This is preferred.
+* If your client uses V2 signatures, or a combination of V2 and V4, or the Arvados token UUID is unknown: use the secret part of the Arvados token for both AccessKey and SecretKey.
diff --git a/doc/api/keep-web-urls.html.textile.liquid b/doc/api/keep-web-urls.html.textile.liquid
new file mode 100644 (file)
index 0000000..91e4f20
--- /dev/null
@@ -0,0 +1,75 @@
+---
+layout: default
+navsection: api
+navmenu: API Methods
+title: "Keep-web URL patterns"
+...
+{% comment %}
+Copyright (C) The Arvados Authors. All rights reserved.
+
+SPDX-License-Identifier: CC-BY-SA-3.0
+{% endcomment %}
+
+Files served by @keep-web@ can be rendered directly in the browser, or @keep-web@ can instruct the browser to only download the file.
+
+When serving files that will render directly in the browser, it is important to properly configure the keep-web service to migitate cross-site-scripting (XSS) attacks.  A HTML page can be stored in a collection.  If an attacker causes a victim to visit that page through Workbench, the HTML will be rendered by the browser.  If all collections are served at the same domain, the browser will consider collections as coming from the same origin, which will grant access to the same browsing data (cookies and local storage).  This would enable malicious Javascript on that page to access Arvados on behalf of the victim.
+
+This can be mitigated by having separate domains for each collection, or limiting preview to circumstances where the collection is not accessed with the user's regular full-access token.  For cluster administrators that understand the risks, this protection can also be turned off.
+
+The following "same origin" URL patterns are supported for public collections and collections shared anonymously via secret links (i.e., collections which can be served by keep-web without making use of any implicit credentials like cookies). See "Same-origin URLs" below.
+
+<pre>
+http://collections.example.com/c=uuid_or_pdh/path/file.txt
+http://collections.example.com/c=uuid_or_pdh/t=TOKEN/path/file.txt
+</pre>
+
+The following "multiple origin" URL patterns are supported for all collections:
+
+<pre>
+http://uuid_or_pdh--collections.example.com/path/file.txt
+http://uuid_or_pdh--collections.example.com/t=TOKEN/path/file.txt
+</pre>
+
+In the "multiple origin" form, the string @--@ can be replaced with @.@ with identical results (assuming the downstream proxy is configured accordingly). These two are equivalent:
+
+<pre>
+http://uuid_or_pdh--collections.example.com/path/file.txt
+http://uuid_or_pdh.collections.example.com/path/file.txt
+</pre>
+
+The first form (with @--@ instead of @.@) avoids the cost and effort of deploying a wildcard TLS certificate for @*.collections.example.com@ at sites that already have a wildcard certificate for @*.example.com@ . The second form is likely to be easier to configure, and more efficient to run, on a downstream proxy.
+
+In all of the above forms, the @collections.example.com@ part can be anything at all: keep-web itself ignores everything after the first @.@ or @--@. (Of course, in order for clients to connect at all, DNS and any relevant proxies must be configured accordingly.)
+
+In all of the above forms, the @uuid_or_pdh@ part can be either a collection UUID or a portable data hash with the @+@ character optionally replaced by @-@ . (When @uuid_or_pdh@ appears in the domain name, replacing @+@ with @-@ is mandatory, because @+@ is not a valid character in a domain name.)
+
+In all of the above forms, a top level directory called @_@ is skipped. In cases where the @path/file.txt@ part might start with @t=@ or @c=@ or @_/@, links should be constructed with a leading @_/@ to ensure the top level directory is not interpreted as a token or collection ID.
+
+Assuming there is a collection with UUID @zzzzz-4zz18-znfnqtbbv4spc3w@ and portable data hash @1f4b0bc7583c2a7f9102c395f4ffc5e3+45@, the following URLs are interchangeable:
+
+<pre>
+http://zzzzz-4zz18-znfnqtbbv4spc3w.collections.example.com/foo/bar.txt
+http://zzzzz-4zz18-znfnqtbbv4spc3w.collections.example.com/_/foo/bar.txt
+http://zzzzz-4zz18-znfnqtbbv4spc3w--collections.example.com/_/foo/bar.txt
+</pre>
+
+The following URLs are read-only, but will return the same content as above:
+
+<pre>
+http://1f4b0bc7583c2a7f9102c395f4ffc5e3-45--foo.example.com/foo/bar.txt
+http://1f4b0bc7583c2a7f9102c395f4ffc5e3-45--.invalid/foo/bar.txt
+http://collections.example.com/by_id/1f4b0bc7583c2a7f9102c395f4ffc5e3%2B45/foo/bar.txt
+http://collections.example.com/by_id/zzzzz-4zz18-znfnqtbbv4spc3w/foo/bar.txt
+</pre>
+
+If the collection is named "MyCollection" and located in a project called "MyProject" which is in the home project of a user with username is "bob", the following read-only URL is also available when authenticating as bob:
+
+pre. http://collections.example.com/users/bob/MyProject/MyCollection/foo/bar.txt
+
+An additional form is supported specifically to make it more convenient to maintain support for existing Workbench download links:
+
+pre. http://collections.example.com/collections/download/uuid_or_pdh/TOKEN/foo/bar.txt
+
+A regular Workbench "download" link is also accepted, but credentials passed via cookie, header, etc. are ignored. Only public data can be served this way:
+
+pre. http://collections.example.com/collections/uuid_or_pdh/foo/bar.txt
diff --git a/doc/api/keep-webdav.html.textile.liquid b/doc/api/keep-webdav.html.textile.liquid
new file mode 100644 (file)
index 0000000..f068a49
--- /dev/null
@@ -0,0 +1,103 @@
+---
+layout: default
+navsection: api
+navmenu: API Methods
+title: "WebDAV"
+...
+
+{% comment %}
+Copyright (C) The Arvados Authors. All rights reserved.
+
+SPDX-License-Identifier: CC-BY-SA-3.0
+{% endcomment %}
+
+"Web Distributed Authoring and Versioning (WebDAV)":https://tools.ietf.org/html/rfc4918 is an IETF standard set of extensions to HTTP to manipulate and retrieve hierarchical web resources, similar to directories in a file system.  Arvados supports accessing files in Keep using WebDAV.
+
+Most major operating systems include built-in support for mounting WebDAV resources as network file systems, see user guide sections for "Windows":{{site.baseurl}}/user/tutorials/tutorial-keep-mount-windows.html , "macOS":{{site.baseurl}}/user/tutorials/tutorial-keep-mount-os-x.html , "Linux (Gnome)":{{site.baseurl}}/user/tutorials/tutorial-keep-mount-gnu-linux.html#gnome .  WebDAV is also supported by various standalone storage browser applications such as "Cyberduck":https://cyberduck.io/ and client libraries exist in many languages for programmatic access.
+
+Keep-web provides read/write HTTP (WebDAV) access to files stored in Keep. It serves public data to anonymous and unauthenticated clients, and serves private data to clients that supply Arvados API tokens.
+
+h3. Supported Operations
+
+Supports WebDAV HTTP methods @GET@, @PUT@, @DELETE@, @PROPFIND@, @COPY@, and @MOVE@.
+
+Does not support @LOCK@ or @UNLOCK@.  These methods will be accepted, but are no-ops.
+
+h3. Browsing
+
+Requests can be authenticated a variety of ways as described below in "Authentication mechanisms":#auth .  An unauthenticated request will return a 401 Unauthorized response with a @WWW-Authenticate@ header indicating "support for RFC 7617 Basic Authentication":https://tools.ietf.org/html/rfc7617 .
+
+Getting a listing from keep-web starting at the root path @/@ will return two folders, @by_id@ and @users@.
+
+The @by_id@ folder will return an empty listing.  However, a path which starts with /by_id/ followed by a collection uuid, portable data hash, or project uuid will return the listing of that object.
+
+The @users@ folder will return a listing of the users for whom the client has permission to read the "home" project of that user.  Browsing an individual user will return the collections and projects directly owned by that user.  Browsing those collections and projects return listings of the files, directories, collections, and subprojects they contain, and so forth.
+
+In addition to the @/by_id/@ path prefix, the collection or project can be specified using a path prefix of @/c=<uuid or pdh>/@ or (if the cluster is properly configured) as a virtual host.  This is described on "Keep-web URLs":keep-web-urls.html
+
+h3(#auth). Authentication mechanisms
+
+A token can be provided in an Authorization header as a @Bearer@ token:
+
+<pre>
+Authorization: Bearer o07j4px7RlJK4CuMYp7C0LDT4CzR1J1qBE5Avo7eCcUjOTikxK
+</pre>
+
+A token can also be provided with "RFC 7617 Basic Authentication":https://tools.ietf.org/html/rfc7617 in this case, the payload is formatted as @username:token@ and encoded with base64.  The username must be non-empty, but is ignored.  In this example, the username is "user":
+
+<pre>
+Authorization: Basic dXNlcjpvMDdqNHB4N1JsSks0Q3VNWXA3QzBMRFQ0Q3pSMUoxcUJFNUF2bzdlQ2NVak9UaWt4Swo=
+</pre>
+
+A base64-encoded token can be provided in a cookie named "api_token":
+
+<pre>
+Cookie: api_token=bzA3ajRweDdSbEpLNEN1TVlwN0MwTERUNEN6UjFKMXFCRTVBdm83ZUNjVWpPVGlreEs=
+</pre>
+
+A token can be provided in an URL-encoded query string:
+
+<pre>
+GET /foo/bar.txt?api_token=o07j4px7RlJK4CuMYp7C0LDT4CzR1J1qBE5Avo7eCcUjOTikxK
+</pre>
+
+A token can be provided in a URL-encoded path (as described in the previous section):
+
+<pre>
+GET /t=o07j4px7RlJK4CuMYp7C0LDT4CzR1J1qBE5Avo7eCcUjOTikxK/_/foo/bar.txt
+</pre>
+
+A suitably encoded token can be provided in a POST body if the request has a content type of application/x-www-form-urlencoded or multipart/form-data:
+
+<pre>
+POST /foo/bar.txt
+Content-Type: application/x-www-form-urlencoded
+[...]
+api_token=o07j4px7RlJK4CuMYp7C0LDT4CzR1J1qBE5Avo7eCcUjOTikxK
+</pre>
+
+If a token is provided in a query string or in a POST request, the response is an HTTP 303 redirect to an equivalent GET request, with the token stripped from the query string and added to a cookie instead.
+
+h3. Indexes
+
+Keep-web returns a generic HTML index listing when a directory is requested with the GET method. It does not serve a default file like "index.html". Directory listings are also returned for WebDAV PROPFIND requests.
+
+h3. Range requests
+
+Keep-web supports partial resource reads using the HTTP @Range@ header as specified in "RFC 7233":https://tools.ietf.org/html/rfc7233 .
+
+h3. Compatibility
+
+Client-provided authorization tokens are ignored if the client does not provide a @Host@ header.
+
+In order to use the query string or a POST form authorization mechanisms, the client must follow 303 redirects; the client must accept cookies with a 303 response and send those cookies when performing the redirect; and either the client or an intervening proxy must resolve a relative URL ("//host/path") if given in a response Location header.
+
+h3. Intranet mode
+
+Normally, Keep-web accepts requests for multiple collections using the same host name, provided the client's credentials are not being used. This provides insufficient XSS protection in an installation where the "anonymously accessible" data is not truly public, but merely protected by network topology.
+
+In such cases -- for example, a site which is not reachable from the internet, where some data is world-readable from Arvados's perspective but is intended to be available only to users within the local network -- the downstream proxy should configured to return 401 for all paths beginning with "/c=".
+
+h3. Same-origin URLs
+
+Without the same-origin protection outlined above, a web page stored in collection X could execute JavaScript code that uses the current viewer's credentials to download additional data from collection Y -- data which is accessible to the current viewer, but not to the author of collection X -- from the same origin (``https://collections.example.com/'') and upload it to some other site chosen by the author of collection X.
index 2653cccd5d257d74d2319d3c1da317b32389b84c..f85e621db45d5d66e127279934b2a503bd7e673a 100644 (file)
@@ -55,6 +55,8 @@ table(table table-bordered table-condensed).
 |recursive|boolean (default false)|Include items owned by subprojects.|query|@true@|
 |exclude_home_project|boolean (default false)|Only return items which are visible to the user but not accessible within the user's home project.  Use this to get a list of items that are shared with the user.  Uses the logic described under the "shared" endpoint.|query|@true@|
 |include|string|If provided with the value "owner_uuid", this will return owner objects in the "included" field of the response.|query||
+|include_trash|boolean (default false)|Include trashed objects.|query|@true@|
+|include_old_versions|boolean (default false)|Include past versions of the collections being listed.|query|@true@|
 
 Notes:
 
index 5b98b8aab754f3e84499d8611398e9ea96b51c26..46cd9fdde459b27b76e004b0dabc95faa2a0540f 100644 (file)
Binary files a/doc/install/new_cluster_checklist_AWS.xlsx and b/doc/install/new_cluster_checklist_AWS.xlsx differ
index 1092a488ba05d52b1ccb17ea671265bf9ed411b9..ba44c43aa59dfa872e8bf3ee60b43d87f7212a32 100644 (file)
Binary files a/doc/install/new_cluster_checklist_Azure.xlsx and b/doc/install/new_cluster_checklist_Azure.xlsx differ
index 4c9951f0c138bcbb2f04f9711a175888f6c832d5..9843f74d17ce5c2ea4466fa9964457269318d3e2 100644 (file)
Binary files a/doc/install/new_cluster_checklist_slurm.xlsx and b/doc/install/new_cluster_checklist_slurm.xlsx differ
index 611c95d2340a3b2da47b8a7cbcfff2a3aad9af8c..b7d918739b86de347b0960e785bbd27dea477fba 100644 (file)
@@ -2,7 +2,7 @@
 //
 // SPDX-License-Identifier: Apache-2.0
 
-// package cmd helps define reusable functions that can be exposed as
+// Package cmd helps define reusable functions that can be exposed as
 // [subcommands of] command line programs.
 package cmd
 
index 6049cba8e403a576b4eba4774a6bfb192f4fa53c..3003ea2df76035cac71ae8bc8916c3e7eaa0a7d4 100644 (file)
@@ -15,3 +15,16 @@ import "context"
 // it to the router package would cause a circular dependency
 // router->arvadostest->ctrlctx->router.)
 type RoutableFunc func(ctx context.Context, opts interface{}) (interface{}, error)
+
+type RoutableFuncWrapper func(RoutableFunc) RoutableFunc
+
+// ComposeWrappers(w1, w2, w3, ...) returns a RoutableFuncWrapper that
+// composes w1, w2, w3, ... such that w1 is the outermost wrapper.
+func ComposeWrappers(wraps ...RoutableFuncWrapper) RoutableFuncWrapper {
+       return func(f RoutableFunc) RoutableFunc {
+               for i := len(wraps) - 1; i >= 0; i-- {
+                       f = wraps[i](f)
+               }
+               return f
+       }
+}
diff --git a/lib/controller/auth_test.go b/lib/controller/auth_test.go
new file mode 100644 (file)
index 0000000..ad214b1
--- /dev/null
@@ -0,0 +1,126 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+package controller
+
+import (
+       "context"
+       "encoding/json"
+       "fmt"
+       "net/http"
+       "net/http/httptest"
+       "os"
+       "time"
+
+       "git.arvados.org/arvados.git/sdk/go/arvados"
+       "git.arvados.org/arvados.git/sdk/go/arvadostest"
+       "git.arvados.org/arvados.git/sdk/go/ctxlog"
+       "git.arvados.org/arvados.git/sdk/go/httpserver"
+       "github.com/sirupsen/logrus"
+       check "gopkg.in/check.v1"
+)
+
+// Gocheck boilerplate
+var _ = check.Suite(&AuthSuite{})
+
+type AuthSuite struct {
+       log logrus.FieldLogger
+       // testServer and testHandler are the controller being tested,
+       // "zhome".
+       testServer  *httpserver.Server
+       testHandler *Handler
+       // remoteServer ("zzzzz") forwards requests to the Rails API
+       // provided by the integration test environment.
+       remoteServer *httpserver.Server
+       // remoteMock ("zmock") appends each incoming request to
+       // remoteMockRequests, and returns 200 with an empty JSON
+       // object.
+       remoteMock         *httpserver.Server
+       remoteMockRequests []http.Request
+
+       fakeProvider *arvadostest.OIDCProvider
+}
+
+func (s *AuthSuite) SetUpTest(c *check.C) {
+       s.log = ctxlog.TestLogger(c)
+
+       s.remoteServer = newServerFromIntegrationTestEnv(c)
+       c.Assert(s.remoteServer.Start(), check.IsNil)
+
+       s.remoteMock = newServerFromIntegrationTestEnv(c)
+       s.remoteMock.Server.Handler = http.HandlerFunc(http.NotFound)
+       c.Assert(s.remoteMock.Start(), check.IsNil)
+
+       s.fakeProvider = arvadostest.NewOIDCProvider(c)
+       s.fakeProvider.AuthEmail = "active-user@arvados.local"
+       s.fakeProvider.AuthEmailVerified = true
+       s.fakeProvider.AuthName = "Fake User Name"
+       s.fakeProvider.ValidCode = fmt.Sprintf("abcdefgh-%d", time.Now().Unix())
+       s.fakeProvider.PeopleAPIResponse = map[string]interface{}{}
+       s.fakeProvider.ValidClientID = "test%client$id"
+       s.fakeProvider.ValidClientSecret = "test#client/secret"
+
+       cluster := &arvados.Cluster{
+               ClusterID:        "zhome",
+               PostgreSQL:       integrationTestCluster().PostgreSQL,
+               ForceLegacyAPI14: forceLegacyAPI14,
+               SystemRootToken:  arvadostest.SystemRootToken,
+       }
+       cluster.TLS.Insecure = true
+       cluster.API.MaxItemsPerResponse = 1000
+       cluster.API.MaxRequestAmplification = 4
+       cluster.API.RequestTimeout = arvados.Duration(5 * time.Minute)
+       arvadostest.SetServiceURL(&cluster.Services.RailsAPI, "https://"+os.Getenv("ARVADOS_TEST_API_HOST"))
+       arvadostest.SetServiceURL(&cluster.Services.Controller, "http://localhost/")
+
+       cluster.RemoteClusters = map[string]arvados.RemoteCluster{
+               "zzzzz": {
+                       Host:   s.remoteServer.Addr,
+                       Proxy:  true,
+                       Scheme: "http",
+               },
+               "zmock": {
+                       Host:   s.remoteMock.Addr,
+                       Proxy:  true,
+                       Scheme: "http",
+               },
+               "*": {
+                       Scheme: "https",
+               },
+       }
+       cluster.Login.OpenIDConnect.Enable = true
+       cluster.Login.OpenIDConnect.Issuer = s.fakeProvider.Issuer.URL
+       cluster.Login.OpenIDConnect.ClientID = s.fakeProvider.ValidClientID
+       cluster.Login.OpenIDConnect.ClientSecret = s.fakeProvider.ValidClientSecret
+       cluster.Login.OpenIDConnect.EmailClaim = "email"
+       cluster.Login.OpenIDConnect.EmailVerifiedClaim = "email_verified"
+
+       s.testHandler = &Handler{Cluster: cluster}
+       s.testServer = newServerFromIntegrationTestEnv(c)
+       s.testServer.Server.Handler = httpserver.HandlerWithContext(
+               ctxlog.Context(context.Background(), s.log),
+               httpserver.AddRequestIDs(httpserver.LogRequests(s.testHandler)))
+       c.Assert(s.testServer.Start(), check.IsNil)
+}
+
+func (s *AuthSuite) TestLocalOIDCAccessToken(c *check.C) {
+       req := httptest.NewRequest("GET", "/arvados/v1/users/current", nil)
+       req.Header.Set("Authorization", "Bearer "+s.fakeProvider.ValidAccessToken())
+       rr := httptest.NewRecorder()
+       s.testServer.Server.Handler.ServeHTTP(rr, req)
+       resp := rr.Result()
+       c.Check(resp.StatusCode, check.Equals, http.StatusOK)
+       var u arvados.User
+       c.Check(json.NewDecoder(resp.Body).Decode(&u), check.IsNil)
+       c.Check(u.UUID, check.Equals, arvadostest.ActiveUserUUID)
+       c.Check(u.OwnerUUID, check.Equals, "zzzzz-tpzed-000000000000000")
+
+       // Request again to exercise cache.
+       req = httptest.NewRequest("GET", "/arvados/v1/users/current", nil)
+       req.Header.Set("Authorization", "Bearer "+s.fakeProvider.ValidAccessToken())
+       rr = httptest.NewRecorder()
+       s.testServer.Server.Handler.ServeHTTP(rr, req)
+       resp = rr.Result()
+       c.Check(resp.StatusCode, check.Equals, http.StatusOK)
+}
index aceaba8087ad2031413516c2671f75174c457fae..cab5e4c4ca45172edb28f07210b001456f1e11af 100644 (file)
@@ -263,17 +263,20 @@ func (h *Handler) saltAuthToken(req *http.Request, remote string) (updatedReq *h
                return updatedReq, nil
        }
 
+       ctxlog.FromContext(req.Context()).Infof("saltAuthToken: cluster %s token %s remote %s", h.Cluster.ClusterID, creds.Tokens[0], remote)
        token, err := auth.SaltToken(creds.Tokens[0], remote)
 
        if err == auth.ErrObsoleteToken {
-               // If the token exists in our own database, salt it
-               // for the remote. Otherwise, assume it was issued by
-               // the remote, and pass it through unmodified.
+               // If the token exists in our own database for our own
+               // user, salt it for the remote. Otherwise, assume it
+               // was issued by the remote, and pass it through
+               // unmodified.
                currentUser, ok, err := h.validateAPItoken(req, creds.Tokens[0])
                if err != nil {
                        return nil, err
-               } else if !ok {
-                       // Not ours; pass through unmodified.
+               } else if !ok || strings.HasPrefix(currentUser.UUID, remote) {
+                       // Unknown, or cached + belongs to remote;
+                       // pass through unmodified.
                        token = creds.Tokens[0]
                } else {
                        // Found; make V2 version and salt it.
index f07c3b63167d577f722b085ff56de252a73f251f..986faa7b05e33e325a6bc1c15b4283ec6d79d9ed 100644 (file)
@@ -79,6 +79,14 @@ func saltedTokenProvider(local backend, remoteID string) rpc.TokenProvider {
                                } else if err != nil {
                                        return nil, err
                                }
+                               if strings.HasPrefix(aca.UUID, remoteID) {
+                                       // We have it cached here, but
+                                       // the token belongs to the
+                                       // remote target itself, so
+                                       // pass it through unmodified.
+                                       tokens = append(tokens, token)
+                                       continue
+                               }
                                salted, err := auth.SaltToken(aca.TokenV2(), remoteID)
                                if err != nil {
                                        return nil, err
index 2dd1d816e060a752fb8e71d4eeaacc5d0b3cfb9b..25bba558dc68096796143b1d9bd4483d07a6f44f 100644 (file)
@@ -14,7 +14,9 @@ import (
        "sync"
        "time"
 
+       "git.arvados.org/arvados.git/lib/controller/api"
        "git.arvados.org/arvados.git/lib/controller/federation"
+       "git.arvados.org/arvados.git/lib/controller/localdb"
        "git.arvados.org/arvados.git/lib/controller/railsproxy"
        "git.arvados.org/arvados.git/lib/controller/router"
        "git.arvados.org/arvados.git/lib/ctrlctx"
@@ -87,7 +89,8 @@ func (h *Handler) setup() {
                Routes: health.Routes{"ping": func() error { _, err := h.db(context.TODO()); return err }},
        })
 
-       rtr := router.New(federation.New(h.Cluster), ctrlctx.WrapCallsInTransactions(h.db))
+       oidcAuthorizer := localdb.OIDCAccessTokenAuthorizer(h.Cluster, h.db)
+       rtr := router.New(federation.New(h.Cluster), api.ComposeWrappers(ctrlctx.WrapCallsInTransactions(h.db), oidcAuthorizer.WrapCalls))
        mux.Handle("/arvados/v1/config", rtr)
        mux.Handle("/"+arvados.EndpointUserAuthenticate.Path, rtr)
 
@@ -103,6 +106,7 @@ func (h *Handler) setup() {
        hs := http.NotFoundHandler()
        hs = prepend(hs, h.proxyRailsAPI)
        hs = h.setupProxyRemoteCluster(hs)
+       hs = prepend(hs, oidcAuthorizer.Middleware)
        mux.Handle("/", hs)
        h.handlerStack = mux
 
index 077493ffc836f58260f1abb19448323ea25f45e9..3da01ca6823562a6b13509adf58b9e621f704dec 100644 (file)
@@ -9,6 +9,7 @@ import (
        "context"
        "encoding/json"
        "io"
+       "io/ioutil"
        "math"
        "net"
        "net/http"
@@ -22,6 +23,7 @@ import (
        "git.arvados.org/arvados.git/lib/service"
        "git.arvados.org/arvados.git/sdk/go/arvados"
        "git.arvados.org/arvados.git/sdk/go/arvadosclient"
+       "git.arvados.org/arvados.git/sdk/go/arvadostest"
        "git.arvados.org/arvados.git/sdk/go/auth"
        "git.arvados.org/arvados.git/sdk/go/ctxlog"
        "git.arvados.org/arvados.git/sdk/go/keepclient"
@@ -38,6 +40,7 @@ type testCluster struct {
 
 type IntegrationSuite struct {
        testClusters map[string]*testCluster
+       oidcprovider *arvadostest.OIDCProvider
 }
 
 func (s *IntegrationSuite) SetUpSuite(c *check.C) {
@@ -47,6 +50,14 @@ func (s *IntegrationSuite) SetUpSuite(c *check.C) {
        }
 
        cwd, _ := os.Getwd()
+
+       s.oidcprovider = arvadostest.NewOIDCProvider(c)
+       s.oidcprovider.AuthEmail = "user@example.com"
+       s.oidcprovider.AuthEmailVerified = true
+       s.oidcprovider.AuthName = "Example User"
+       s.oidcprovider.ValidClientID = "clientid"
+       s.oidcprovider.ValidClientSecret = "clientsecret"
+
        s.testClusters = map[string]*testCluster{
                "z1111": nil,
                "z2222": nil,
@@ -105,6 +116,24 @@ func (s *IntegrationSuite) SetUpSuite(c *check.C) {
         ActivateUsers: true
 `
                }
+               if id == "z1111" {
+                       yaml += `
+    Login:
+      LoginCluster: z1111
+      OpenIDConnect:
+        Enable: true
+        Issuer: ` + s.oidcprovider.Issuer.URL + `
+        ClientID: ` + s.oidcprovider.ValidClientID + `
+        ClientSecret: ` + s.oidcprovider.ValidClientSecret + `
+        EmailClaim: email
+        EmailVerifiedClaim: email_verified
+`
+               } else {
+                       yaml += `
+    Login:
+      LoginCluster: z1111
+`
+               }
 
                loader := config.NewLoader(bytes.NewBufferString(yaml), ctxlog.TestLogger(c))
                loader.Path = "-"
@@ -520,3 +549,55 @@ func (s *IntegrationSuite) TestSetupUserWithVM(c *check.C) {
 
        c.Check(len(outLinks.Items), check.Equals, 1)
 }
+
+func (s *IntegrationSuite) TestOIDCAccessTokenAuth(c *check.C) {
+       conn1 := s.conn("z1111")
+       rootctx1, _, _ := s.rootClients("z1111")
+       s.userClients(rootctx1, c, conn1, "z1111", true)
+
+       accesstoken := s.oidcprovider.ValidAccessToken()
+
+       for _, clusterid := range []string{"z1111", "z2222"} {
+               c.Logf("trying clusterid %s", clusterid)
+
+               conn := s.conn(clusterid)
+               ctx, ac, kc := s.clientsWithToken(clusterid, accesstoken)
+
+               var coll arvados.Collection
+
+               // Write some file data and create a collection
+               {
+                       fs, err := coll.FileSystem(ac, kc)
+                       c.Assert(err, check.IsNil)
+                       f, err := fs.OpenFile("test.txt", os.O_CREATE|os.O_RDWR, 0777)
+                       c.Assert(err, check.IsNil)
+                       _, err = io.WriteString(f, "IntegrationSuite.TestOIDCAccessTokenAuth")
+                       c.Assert(err, check.IsNil)
+                       err = f.Close()
+                       c.Assert(err, check.IsNil)
+                       mtxt, err := fs.MarshalManifest(".")
+                       c.Assert(err, check.IsNil)
+                       coll, err = conn.CollectionCreate(ctx, arvados.CreateOptions{Attrs: map[string]interface{}{
+                               "manifest_text": mtxt,
+                       }})
+                       c.Assert(err, check.IsNil)
+               }
+
+               // Read the collection & file data
+               {
+                       user, err := conn.UserGetCurrent(ctx, arvados.GetOptions{})
+                       c.Assert(err, check.IsNil)
+                       c.Check(user.FullName, check.Equals, "Example User")
+                       coll, err = conn.CollectionGet(ctx, arvados.GetOptions{UUID: coll.UUID})
+                       c.Assert(err, check.IsNil)
+                       c.Check(coll.ManifestText, check.Not(check.Equals), "")
+                       fs, err := coll.FileSystem(ac, kc)
+                       c.Assert(err, check.IsNil)
+                       f, err := fs.Open("test.txt")
+                       c.Assert(err, check.IsNil)
+                       buf, err := ioutil.ReadAll(f)
+                       c.Assert(err, check.IsNil)
+                       c.Check(buf, check.DeepEquals, []byte("IntegrationSuite.TestOIDCAccessTokenAuth"))
+               }
+       }
+}
index e0b01f13ebee8c4f01084d0dc4c8dca76804e696..2da7ca5ccf98e14a6c2cb32d4602ecb81eb68101 100644 (file)
@@ -9,9 +9,11 @@ import (
        "context"
        "crypto/hmac"
        "crypto/sha256"
+       "database/sql"
        "encoding/base64"
        "errors"
        "fmt"
+       "io"
        "net/http"
        "net/url"
        "strings"
@@ -19,17 +21,29 @@ import (
        "text/template"
        "time"
 
+       "git.arvados.org/arvados.git/lib/controller/api"
+       "git.arvados.org/arvados.git/lib/controller/railsproxy"
        "git.arvados.org/arvados.git/lib/controller/rpc"
+       "git.arvados.org/arvados.git/lib/ctrlctx"
        "git.arvados.org/arvados.git/sdk/go/arvados"
        "git.arvados.org/arvados.git/sdk/go/auth"
        "git.arvados.org/arvados.git/sdk/go/ctxlog"
        "git.arvados.org/arvados.git/sdk/go/httpserver"
        "github.com/coreos/go-oidc"
+       lru "github.com/hashicorp/golang-lru"
+       "github.com/jmoiron/sqlx"
+       "github.com/sirupsen/logrus"
        "golang.org/x/oauth2"
        "google.golang.org/api/option"
        "google.golang.org/api/people/v1"
 )
 
+const (
+       tokenCacheSize        = 1000
+       tokenCacheNegativeTTL = time.Minute * 5
+       tokenCacheTTL         = time.Minute * 10
+)
+
 type oidcLoginController struct {
        Cluster            *arvados.Cluster
        RailsProxy         *railsProxy
@@ -139,17 +153,23 @@ func (ctrl *oidcLoginController) UserAuthenticate(ctx context.Context, opts arva
        return arvados.APIClientAuthorization{}, httpserver.ErrorWithStatus(errors.New("username/password authentication is not available"), http.StatusBadRequest)
 }
 
+// claimser can decode arbitrary claims into a map. Implemented by
+// *oauth2.IDToken and *oauth2.UserInfo.
+type claimser interface {
+       Claims(interface{}) error
+}
+
 // Use a person's token to get all of their email addresses, with the
 // primary address at index 0. The provided defaultAddr is always
 // included in the returned slice, and is used as the primary if the
 // Google API does not indicate one.
-func (ctrl *oidcLoginController) getAuthInfo(ctx context.Context, token *oauth2.Token, idToken *oidc.IDToken) (*rpc.UserSessionAuthInfo, error) {
+func (ctrl *oidcLoginController) getAuthInfo(ctx context.Context, token *oauth2.Token, claimser claimser) (*rpc.UserSessionAuthInfo, error) {
        var ret rpc.UserSessionAuthInfo
        defer ctxlog.FromContext(ctx).WithField("ret", &ret).Debug("getAuthInfo returned")
 
        var claims map[string]interface{}
-       if err := idToken.Claims(&claims); err != nil {
-               return nil, fmt.Errorf("error extracting claims from ID token: %s", err)
+       if err := claimser.Claims(&claims); err != nil {
+               return nil, fmt.Errorf("error extracting claims from token: %s", err)
        } else if verified, _ := claims[ctrl.EmailVerifiedClaim].(bool); verified || ctrl.EmailVerifiedClaim == "" {
                // Fall back to this info if the People API call
                // (below) doesn't return a primary && verified email.
@@ -297,3 +317,179 @@ func (s oauth2State) computeHMAC(key []byte) []byte {
        fmt.Fprintf(mac, "%x %s %s", s.Time, s.Remote, s.ReturnTo)
        return mac.Sum(nil)
 }
+
+func OIDCAccessTokenAuthorizer(cluster *arvados.Cluster, getdb func(context.Context) (*sqlx.DB, error)) *oidcTokenAuthorizer {
+       // We want ctrl to be nil if the chosen controller is not a
+       // *oidcLoginController, so we can ignore the 2nd return value
+       // of this type cast.
+       ctrl, _ := chooseLoginController(cluster, railsproxy.NewConn(cluster)).(*oidcLoginController)
+       cache, err := lru.New2Q(tokenCacheSize)
+       if err != nil {
+               panic(err)
+       }
+       return &oidcTokenAuthorizer{
+               ctrl:  ctrl,
+               getdb: getdb,
+               cache: cache,
+       }
+}
+
+type oidcTokenAuthorizer struct {
+       ctrl  *oidcLoginController
+       getdb func(context.Context) (*sqlx.DB, error)
+       cache *lru.TwoQueueCache
+}
+
+func (ta *oidcTokenAuthorizer) Middleware(w http.ResponseWriter, r *http.Request, next http.Handler) {
+       if ta.ctrl == nil {
+               // Not using a compatible (OIDC) login controller.
+       } else if authhdr := strings.Split(r.Header.Get("Authorization"), " "); len(authhdr) > 1 && (authhdr[0] == "OAuth2" || authhdr[0] == "Bearer") {
+               err := ta.registerToken(r.Context(), authhdr[1])
+               if err != nil {
+                       http.Error(w, err.Error(), http.StatusInternalServerError)
+                       return
+               }
+       }
+       next.ServeHTTP(w, r)
+}
+
+func (ta *oidcTokenAuthorizer) WrapCalls(origFunc api.RoutableFunc) api.RoutableFunc {
+       if ta.ctrl == nil {
+               // Not using a compatible (OIDC) login controller.
+               return origFunc
+       }
+       return func(ctx context.Context, opts interface{}) (_ interface{}, err error) {
+               creds, ok := auth.FromContext(ctx)
+               if !ok {
+                       return origFunc(ctx, opts)
+               }
+               // Check each token in the incoming request. If any
+               // are OAuth2 access tokens, swap them out for Arvados
+               // tokens.
+               for _, tok := range creds.Tokens {
+                       err = ta.registerToken(ctx, tok)
+                       if err != nil {
+                               return nil, err
+                       }
+               }
+               return origFunc(ctx, opts)
+       }
+}
+
+// registerToken checks whether tok is a valid OIDC Access Token and,
+// if so, ensures that an api_client_authorizations row exists so that
+// RailsAPI will accept it as an Arvados token.
+func (ta *oidcTokenAuthorizer) registerToken(ctx context.Context, tok string) error {
+       if tok == ta.ctrl.Cluster.SystemRootToken || strings.HasPrefix(tok, "v2/") {
+               return nil
+       }
+       if cached, hit := ta.cache.Get(tok); !hit {
+               // Fall through to database and OIDC provider checks
+               // below
+       } else if exp, ok := cached.(time.Time); ok {
+               // cached negative result (value is expiry time)
+               if time.Now().Before(exp) {
+                       return nil
+               } else {
+                       ta.cache.Remove(tok)
+               }
+       } else {
+               // cached positive result
+               aca := cached.(arvados.APIClientAuthorization)
+               var expiring bool
+               if aca.ExpiresAt != "" {
+                       t, err := time.Parse(time.RFC3339Nano, aca.ExpiresAt)
+                       if err != nil {
+                               return fmt.Errorf("error parsing expires_at value: %w", err)
+                       }
+                       expiring = t.Before(time.Now().Add(time.Minute))
+               }
+               if !expiring {
+                       return nil
+               }
+       }
+
+       db, err := ta.getdb(ctx)
+       if err != nil {
+               return err
+       }
+       tx, err := db.Beginx()
+       if err != nil {
+               return err
+       }
+       defer tx.Rollback()
+       ctx = ctrlctx.NewWithTransaction(ctx, tx)
+
+       // We use hmac-sha256(accesstoken,systemroottoken) as the
+       // secret part of our own token, and avoid storing the auth
+       // provider's real secret in our database.
+       mac := hmac.New(sha256.New, []byte(ta.ctrl.Cluster.SystemRootToken))
+       io.WriteString(mac, tok)
+       hmac := fmt.Sprintf("%x", mac.Sum(nil))
+
+       var expiring bool
+       err = tx.QueryRowContext(ctx, `select (expires_at is not null and expires_at - interval '1 minute' <= current_timestamp at time zone 'UTC') from api_client_authorizations where api_token=$1`, hmac).Scan(&expiring)
+       if err != nil && err != sql.ErrNoRows {
+               return fmt.Errorf("database error while checking token: %w", err)
+       } else if err == nil && !expiring {
+               // Token is already in the database as an Arvados
+               // token, and isn't about to expire, so we can pass it
+               // through to RailsAPI etc. regardless of whether it's
+               // an OIDC access token.
+               return nil
+       }
+       updating := err == nil
+
+       // Check whether the token is a valid OIDC access token. If
+       // so, swap it out for an Arvados token (creating/updating an
+       // api_client_authorizations row if needed) which downstream
+       // server components will accept.
+       err = ta.ctrl.setup()
+       if err != nil {
+               return fmt.Errorf("error setting up OpenID Connect provider: %s", err)
+       }
+       oauth2Token := &oauth2.Token{
+               AccessToken: tok,
+       }
+       userinfo, err := ta.ctrl.provider.UserInfo(ctx, oauth2.StaticTokenSource(oauth2Token))
+       if err != nil {
+               ta.cache.Add(tok, time.Now().Add(tokenCacheNegativeTTL))
+               return nil
+       }
+       ctxlog.FromContext(ctx).WithField("userinfo", userinfo).Debug("(*oidcTokenAuthorizer)registerToken: got userinfo")
+       authinfo, err := ta.ctrl.getAuthInfo(ctx, oauth2Token, userinfo)
+       if err != nil {
+               return err
+       }
+
+       // Expiry time for our token is one minute longer than our
+       // cache TTL, so we don't pass it through to RailsAPI just as
+       // it's expiring.
+       exp := time.Now().UTC().Add(tokenCacheTTL + time.Minute)
+
+       var aca arvados.APIClientAuthorization
+       if updating {
+               _, err = tx.ExecContext(ctx, `update api_client_authorizations set expires_at=$1 where api_token=$2`, exp, hmac)
+               if err != nil {
+                       return fmt.Errorf("error updating token expiry time: %w", err)
+               }
+               ctxlog.FromContext(ctx).WithField("HMAC", hmac).Debug("(*oidcTokenAuthorizer)registerToken: updated api_client_authorizations row")
+       } else {
+               aca, err = createAPIClientAuthorization(ctx, ta.ctrl.RailsProxy, ta.ctrl.Cluster.SystemRootToken, *authinfo)
+               if err != nil {
+                       return err
+               }
+               _, err = tx.ExecContext(ctx, `update api_client_authorizations set api_token=$1, expires_at=$2 where uuid=$3`, hmac, exp, aca.UUID)
+               if err != nil {
+                       return fmt.Errorf("error adding OIDC access token to database: %w", err)
+               }
+               aca.APIToken = hmac
+               ctxlog.FromContext(ctx).WithFields(logrus.Fields{"UUID": aca.UUID, "HMAC": hmac}).Debug("(*oidcTokenAuthorizer)registerToken: inserted api_client_authorizations row")
+       }
+       err = tx.Commit()
+       if err != nil {
+               return err
+       }
+       ta.cache.Add(tok, aca)
+       return nil
+}
index 2ccb1fce2a1e9dc42592f0543b2b0fc03f7d6fea..9bc6f90ea9c35b9d9de4d8fa5bdee029aaa206a2 100644 (file)
@@ -7,9 +7,6 @@ package localdb
 import (
        "bytes"
        "context"
-       "crypto/rand"
-       "crypto/rsa"
-       "encoding/base64"
        "encoding/json"
        "fmt"
        "net/http"
@@ -27,7 +24,6 @@ import (
        "git.arvados.org/arvados.git/sdk/go/auth"
        "git.arvados.org/arvados.git/sdk/go/ctxlog"
        check "gopkg.in/check.v1"
-       jose "gopkg.in/square/go-jose.v2"
 )
 
 // Gocheck boilerplate
@@ -38,22 +34,10 @@ func Test(t *testing.T) {
 var _ = check.Suite(&OIDCLoginSuite{})
 
 type OIDCLoginSuite struct {
-       cluster               *arvados.Cluster
-       localdb               *Conn
-       railsSpy              *arvadostest.Proxy
-       fakeIssuer            *httptest.Server
-       fakePeopleAPI         *httptest.Server
-       fakePeopleAPIResponse map[string]interface{}
-       issuerKey             *rsa.PrivateKey
-
-       // expected token request
-       validCode         string
-       validClientID     string
-       validClientSecret string
-       // desired response from token endpoint
-       authEmail         string
-       authEmailVerified bool
-       authName          string
+       cluster      *arvados.Cluster
+       localdb      *Conn
+       railsSpy     *arvadostest.Proxy
+       fakeProvider *arvadostest.OIDCProvider
 }
 
 func (s *OIDCLoginSuite) TearDownSuite(c *check.C) {
@@ -64,103 +48,12 @@ func (s *OIDCLoginSuite) TearDownSuite(c *check.C) {
 }
 
 func (s *OIDCLoginSuite) SetUpTest(c *check.C) {
-       var err error
-       s.issuerKey, err = rsa.GenerateKey(rand.Reader, 2048)
-       c.Assert(err, check.IsNil)
-
-       s.authEmail = "active-user@arvados.local"
-       s.authEmailVerified = true
-       s.authName = "Fake User Name"
-       s.fakeIssuer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
-               req.ParseForm()
-               c.Logf("fakeIssuer: got req: %s %s %s", req.Method, req.URL, req.Form)
-               w.Header().Set("Content-Type", "application/json")
-               switch req.URL.Path {
-               case "/.well-known/openid-configuration":
-                       json.NewEncoder(w).Encode(map[string]interface{}{
-                               "issuer":                 s.fakeIssuer.URL,
-                               "authorization_endpoint": s.fakeIssuer.URL + "/auth",
-                               "token_endpoint":         s.fakeIssuer.URL + "/token",
-                               "jwks_uri":               s.fakeIssuer.URL + "/jwks",
-                               "userinfo_endpoint":      s.fakeIssuer.URL + "/userinfo",
-                       })
-               case "/token":
-                       var clientID, clientSecret string
-                       auth, _ := base64.StdEncoding.DecodeString(strings.TrimPrefix(req.Header.Get("Authorization"), "Basic "))
-                       authsplit := strings.Split(string(auth), ":")
-                       if len(authsplit) == 2 {
-                               clientID, _ = url.QueryUnescape(authsplit[0])
-                               clientSecret, _ = url.QueryUnescape(authsplit[1])
-                       }
-                       if clientID != s.validClientID || clientSecret != s.validClientSecret {
-                               c.Logf("fakeIssuer: expected (%q, %q) got (%q, %q)", s.validClientID, s.validClientSecret, clientID, clientSecret)
-                               w.WriteHeader(http.StatusUnauthorized)
-                               return
-                       }
-
-                       if req.Form.Get("code") != s.validCode || s.validCode == "" {
-                               w.WriteHeader(http.StatusUnauthorized)
-                               return
-                       }
-                       idToken, _ := json.Marshal(map[string]interface{}{
-                               "iss":            s.fakeIssuer.URL,
-                               "aud":            []string{clientID},
-                               "sub":            "fake-user-id",
-                               "exp":            time.Now().UTC().Add(time.Minute).Unix(),
-                               "iat":            time.Now().UTC().Unix(),
-                               "nonce":          "fake-nonce",
-                               "email":          s.authEmail,
-                               "email_verified": s.authEmailVerified,
-                               "name":           s.authName,
-                               "alt_verified":   true,                    // for custom claim tests
-                               "alt_email":      "alt_email@example.com", // for custom claim tests
-                               "alt_username":   "desired-username",      // for custom claim tests
-                       })
-                       json.NewEncoder(w).Encode(struct {
-                               AccessToken  string `json:"access_token"`
-                               TokenType    string `json:"token_type"`
-                               RefreshToken string `json:"refresh_token"`
-                               ExpiresIn    int32  `json:"expires_in"`
-                               IDToken      string `json:"id_token"`
-                       }{
-                               AccessToken:  s.fakeToken(c, []byte("fake access token")),
-                               TokenType:    "Bearer",
-                               RefreshToken: "test-refresh-token",
-                               ExpiresIn:    30,
-                               IDToken:      s.fakeToken(c, idToken),
-                       })
-               case "/jwks":
-                       json.NewEncoder(w).Encode(jose.JSONWebKeySet{
-                               Keys: []jose.JSONWebKey{
-                                       {Key: s.issuerKey.Public(), Algorithm: string(jose.RS256), KeyID: ""},
-                               },
-                       })
-               case "/auth":
-                       w.WriteHeader(http.StatusInternalServerError)
-               case "/userinfo":
-                       w.WriteHeader(http.StatusInternalServerError)
-               default:
-                       w.WriteHeader(http.StatusNotFound)
-               }
-       }))
-       s.validCode = fmt.Sprintf("abcdefgh-%d", time.Now().Unix())
-
-       s.fakePeopleAPI = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
-               req.ParseForm()
-               c.Logf("fakePeopleAPI: got req: %s %s %s", req.Method, req.URL, req.Form)
-               w.Header().Set("Content-Type", "application/json")
-               switch req.URL.Path {
-               case "/v1/people/me":
-                       if f := req.Form.Get("personFields"); f != "emailAddresses,names" {
-                               w.WriteHeader(http.StatusBadRequest)
-                               break
-                       }
-                       json.NewEncoder(w).Encode(s.fakePeopleAPIResponse)
-               default:
-                       w.WriteHeader(http.StatusNotFound)
-               }
-       }))
-       s.fakePeopleAPIResponse = map[string]interface{}{}
+       s.fakeProvider = arvadostest.NewOIDCProvider(c)
+       s.fakeProvider.AuthEmail = "active-user@arvados.local"
+       s.fakeProvider.AuthEmailVerified = true
+       s.fakeProvider.AuthName = "Fake User Name"
+       s.fakeProvider.ValidCode = fmt.Sprintf("abcdefgh-%d", time.Now().Unix())
+       s.fakeProvider.PeopleAPIResponse = map[string]interface{}{}
 
        cfg, err := config.NewLoader(nil, ctxlog.TestLogger(c)).Load()
        c.Assert(err, check.IsNil)
@@ -171,13 +64,13 @@ func (s *OIDCLoginSuite) SetUpTest(c *check.C) {
        s.cluster.Login.Google.ClientID = "test%client$id"
        s.cluster.Login.Google.ClientSecret = "test#client/secret"
        s.cluster.Users.PreferDomainForUsername = "PreferDomainForUsername.example.com"
-       s.validClientID = "test%client$id"
-       s.validClientSecret = "test#client/secret"
+       s.fakeProvider.ValidClientID = "test%client$id"
+       s.fakeProvider.ValidClientSecret = "test#client/secret"
 
        s.localdb = NewConn(s.cluster)
        c.Assert(s.localdb.loginController, check.FitsTypeOf, (*oidcLoginController)(nil))
-       s.localdb.loginController.(*oidcLoginController).Issuer = s.fakeIssuer.URL
-       s.localdb.loginController.(*oidcLoginController).peopleAPIBasePath = s.fakePeopleAPI.URL
+       s.localdb.loginController.(*oidcLoginController).Issuer = s.fakeProvider.Issuer.URL
+       s.localdb.loginController.(*oidcLoginController).peopleAPIBasePath = s.fakeProvider.PeopleAPI.URL
 
        s.railsSpy = arvadostest.NewProxy(c, s.cluster.Services.RailsAPI)
        *s.localdb.railsProxy = *rpc.NewConn(s.cluster.ClusterID, s.railsSpy.URL, true, rpc.PassthroughTokenProvider)
@@ -206,7 +99,7 @@ func (s *OIDCLoginSuite) TestGoogleLogin_Start(c *check.C) {
                c.Check(err, check.IsNil)
                target, err := url.Parse(resp.RedirectLocation)
                c.Check(err, check.IsNil)
-               issuerURL, _ := url.Parse(s.fakeIssuer.URL)
+               issuerURL, _ := url.Parse(s.fakeProvider.Issuer.URL)
                c.Check(target.Host, check.Equals, issuerURL.Host)
                q := target.Query()
                c.Check(q.Get("client_id"), check.Equals, "test%client$id")
@@ -232,7 +125,7 @@ func (s *OIDCLoginSuite) TestGoogleLogin_InvalidCode(c *check.C) {
 func (s *OIDCLoginSuite) TestGoogleLogin_InvalidState(c *check.C) {
        s.startLogin(c)
        resp, err := s.localdb.Login(context.Background(), arvados.LoginOptions{
-               Code:  s.validCode,
+               Code:  s.fakeProvider.ValidCode,
                State: "bogus-state",
        })
        c.Check(err, check.IsNil)
@@ -241,20 +134,20 @@ func (s *OIDCLoginSuite) TestGoogleLogin_InvalidState(c *check.C) {
 }
 
 func (s *OIDCLoginSuite) setupPeopleAPIError(c *check.C) {
-       s.fakePeopleAPI = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
+       s.fakeProvider.PeopleAPI = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
                w.WriteHeader(http.StatusForbidden)
                fmt.Fprintln(w, `Error 403: accessNotConfigured`)
        }))
-       s.localdb.loginController.(*oidcLoginController).peopleAPIBasePath = s.fakePeopleAPI.URL
+       s.localdb.loginController.(*oidcLoginController).peopleAPIBasePath = s.fakeProvider.PeopleAPI.URL
 }
 
 func (s *OIDCLoginSuite) TestGoogleLogin_PeopleAPIDisabled(c *check.C) {
        s.localdb.loginController.(*oidcLoginController).UseGooglePeopleAPI = false
-       s.authEmail = "joe.smith@primary.example.com"
+       s.fakeProvider.AuthEmail = "joe.smith@primary.example.com"
        s.setupPeopleAPIError(c)
        state := s.startLogin(c)
        _, err := s.localdb.Login(context.Background(), arvados.LoginOptions{
-               Code:  s.validCode,
+               Code:  s.fakeProvider.ValidCode,
                State: state,
        })
        c.Check(err, check.IsNil)
@@ -294,7 +187,7 @@ func (s *OIDCLoginSuite) TestGoogleLogin_PeopleAPIError(c *check.C) {
        s.setupPeopleAPIError(c)
        state := s.startLogin(c)
        resp, err := s.localdb.Login(context.Background(), arvados.LoginOptions{
-               Code:  s.validCode,
+               Code:  s.fakeProvider.ValidCode,
                State: state,
        })
        c.Check(err, check.IsNil)
@@ -304,11 +197,11 @@ func (s *OIDCLoginSuite) TestGoogleLogin_PeopleAPIError(c *check.C) {
 func (s *OIDCLoginSuite) TestGenericOIDCLogin(c *check.C) {
        s.cluster.Login.Google.Enable = false
        s.cluster.Login.OpenIDConnect.Enable = true
-       json.Unmarshal([]byte(fmt.Sprintf("%q", s.fakeIssuer.URL)), &s.cluster.Login.OpenIDConnect.Issuer)
+       json.Unmarshal([]byte(fmt.Sprintf("%q", s.fakeProvider.Issuer.URL)), &s.cluster.Login.OpenIDConnect.Issuer)
        s.cluster.Login.OpenIDConnect.ClientID = "oidc#client#id"
        s.cluster.Login.OpenIDConnect.ClientSecret = "oidc#client#secret"
-       s.validClientID = "oidc#client#id"
-       s.validClientSecret = "oidc#client#secret"
+       s.fakeProvider.ValidClientID = "oidc#client#id"
+       s.fakeProvider.ValidClientSecret = "oidc#client#secret"
        for _, trial := range []struct {
                expectEmail string // "" if failure expected
                setup       func()
@@ -317,8 +210,8 @@ func (s *OIDCLoginSuite) TestGenericOIDCLogin(c *check.C) {
                        expectEmail: "user@oidc.example.com",
                        setup: func() {
                                c.Log("=== succeed because email_verified is false but not required")
-                               s.authEmail = "user@oidc.example.com"
-                               s.authEmailVerified = false
+                               s.fakeProvider.AuthEmail = "user@oidc.example.com"
+                               s.fakeProvider.AuthEmailVerified = false
                                s.cluster.Login.OpenIDConnect.EmailClaim = "email"
                                s.cluster.Login.OpenIDConnect.EmailVerifiedClaim = ""
                                s.cluster.Login.OpenIDConnect.UsernameClaim = ""
@@ -328,8 +221,8 @@ func (s *OIDCLoginSuite) TestGenericOIDCLogin(c *check.C) {
                        expectEmail: "",
                        setup: func() {
                                c.Log("=== fail because email_verified is false and required")
-                               s.authEmail = "user@oidc.example.com"
-                               s.authEmailVerified = false
+                               s.fakeProvider.AuthEmail = "user@oidc.example.com"
+                               s.fakeProvider.AuthEmailVerified = false
                                s.cluster.Login.OpenIDConnect.EmailClaim = "email"
                                s.cluster.Login.OpenIDConnect.EmailVerifiedClaim = "email_verified"
                                s.cluster.Login.OpenIDConnect.UsernameClaim = ""
@@ -339,8 +232,8 @@ func (s *OIDCLoginSuite) TestGenericOIDCLogin(c *check.C) {
                        expectEmail: "user@oidc.example.com",
                        setup: func() {
                                c.Log("=== succeed because email_verified is false but config uses custom 'verified' claim")
-                               s.authEmail = "user@oidc.example.com"
-                               s.authEmailVerified = false
+                               s.fakeProvider.AuthEmail = "user@oidc.example.com"
+                               s.fakeProvider.AuthEmailVerified = false
                                s.cluster.Login.OpenIDConnect.EmailClaim = "email"
                                s.cluster.Login.OpenIDConnect.EmailVerifiedClaim = "alt_verified"
                                s.cluster.Login.OpenIDConnect.UsernameClaim = ""
@@ -350,8 +243,8 @@ func (s *OIDCLoginSuite) TestGenericOIDCLogin(c *check.C) {
                        expectEmail: "alt_email@example.com",
                        setup: func() {
                                c.Log("=== succeed with custom 'email' and 'email_verified' claims")
-                               s.authEmail = "bad@wrong.example.com"
-                               s.authEmailVerified = false
+                               s.fakeProvider.AuthEmail = "bad@wrong.example.com"
+                               s.fakeProvider.AuthEmailVerified = false
                                s.cluster.Login.OpenIDConnect.EmailClaim = "alt_email"
                                s.cluster.Login.OpenIDConnect.EmailVerifiedClaim = "alt_verified"
                                s.cluster.Login.OpenIDConnect.UsernameClaim = "alt_username"
@@ -368,7 +261,7 @@ func (s *OIDCLoginSuite) TestGenericOIDCLogin(c *check.C) {
 
                state := s.startLogin(c)
                resp, err := s.localdb.Login(context.Background(), arvados.LoginOptions{
-                       Code:  s.validCode,
+                       Code:  s.fakeProvider.ValidCode,
                        State: state,
                })
                c.Assert(err, check.IsNil)
@@ -399,7 +292,7 @@ func (s *OIDCLoginSuite) TestGenericOIDCLogin(c *check.C) {
 func (s *OIDCLoginSuite) TestGoogleLogin_Success(c *check.C) {
        state := s.startLogin(c)
        resp, err := s.localdb.Login(context.Background(), arvados.LoginOptions{
-               Code:  s.validCode,
+               Code:  s.fakeProvider.ValidCode,
                State: state,
        })
        c.Check(err, check.IsNil)
@@ -436,8 +329,8 @@ func (s *OIDCLoginSuite) TestGoogleLogin_Success(c *check.C) {
 }
 
 func (s *OIDCLoginSuite) TestGoogleLogin_RealName(c *check.C) {
-       s.authEmail = "joe.smith@primary.example.com"
-       s.fakePeopleAPIResponse = map[string]interface{}{
+       s.fakeProvider.AuthEmail = "joe.smith@primary.example.com"
+       s.fakeProvider.PeopleAPIResponse = map[string]interface{}{
                "names": []map[string]interface{}{
                        {
                                "metadata":   map[string]interface{}{"primary": false},
@@ -453,7 +346,7 @@ func (s *OIDCLoginSuite) TestGoogleLogin_RealName(c *check.C) {
        }
        state := s.startLogin(c)
        s.localdb.Login(context.Background(), arvados.LoginOptions{
-               Code:  s.validCode,
+               Code:  s.fakeProvider.ValidCode,
                State: state,
        })
 
@@ -463,11 +356,11 @@ func (s *OIDCLoginSuite) TestGoogleLogin_RealName(c *check.C) {
 }
 
 func (s *OIDCLoginSuite) TestGoogleLogin_OIDCRealName(c *check.C) {
-       s.authName = "Joe P. Smith"
-       s.authEmail = "joe.smith@primary.example.com"
+       s.fakeProvider.AuthName = "Joe P. Smith"
+       s.fakeProvider.AuthEmail = "joe.smith@primary.example.com"
        state := s.startLogin(c)
        s.localdb.Login(context.Background(), arvados.LoginOptions{
-               Code:  s.validCode,
+               Code:  s.fakeProvider.ValidCode,
                State: state,
        })
 
@@ -478,8 +371,8 @@ func (s *OIDCLoginSuite) TestGoogleLogin_OIDCRealName(c *check.C) {
 
 // People API returns some additional email addresses.
 func (s *OIDCLoginSuite) TestGoogleLogin_AlternateEmailAddresses(c *check.C) {
-       s.authEmail = "joe.smith@primary.example.com"
-       s.fakePeopleAPIResponse = map[string]interface{}{
+       s.fakeProvider.AuthEmail = "joe.smith@primary.example.com"
+       s.fakeProvider.PeopleAPIResponse = map[string]interface{}{
                "emailAddresses": []map[string]interface{}{
                        {
                                "metadata": map[string]interface{}{"verified": true},
@@ -496,7 +389,7 @@ func (s *OIDCLoginSuite) TestGoogleLogin_AlternateEmailAddresses(c *check.C) {
        }
        state := s.startLogin(c)
        s.localdb.Login(context.Background(), arvados.LoginOptions{
-               Code:  s.validCode,
+               Code:  s.fakeProvider.ValidCode,
                State: state,
        })
 
@@ -507,8 +400,8 @@ func (s *OIDCLoginSuite) TestGoogleLogin_AlternateEmailAddresses(c *check.C) {
 
 // Primary address is not the one initially returned by oidc.
 func (s *OIDCLoginSuite) TestGoogleLogin_AlternateEmailAddresses_Primary(c *check.C) {
-       s.authEmail = "joe.smith@alternate.example.com"
-       s.fakePeopleAPIResponse = map[string]interface{}{
+       s.fakeProvider.AuthEmail = "joe.smith@alternate.example.com"
+       s.fakeProvider.PeopleAPIResponse = map[string]interface{}{
                "emailAddresses": []map[string]interface{}{
                        {
                                "metadata": map[string]interface{}{"verified": true, "primary": true},
@@ -526,7 +419,7 @@ func (s *OIDCLoginSuite) TestGoogleLogin_AlternateEmailAddresses_Primary(c *chec
        }
        state := s.startLogin(c)
        s.localdb.Login(context.Background(), arvados.LoginOptions{
-               Code:  s.validCode,
+               Code:  s.fakeProvider.ValidCode,
                State: state,
        })
        authinfo := getCallbackAuthInfo(c, s.railsSpy)
@@ -536,9 +429,9 @@ func (s *OIDCLoginSuite) TestGoogleLogin_AlternateEmailAddresses_Primary(c *chec
 }
 
 func (s *OIDCLoginSuite) TestGoogleLogin_NoPrimaryEmailAddress(c *check.C) {
-       s.authEmail = "joe.smith@unverified.example.com"
-       s.authEmailVerified = false
-       s.fakePeopleAPIResponse = map[string]interface{}{
+       s.fakeProvider.AuthEmail = "joe.smith@unverified.example.com"
+       s.fakeProvider.AuthEmailVerified = false
+       s.fakeProvider.PeopleAPIResponse = map[string]interface{}{
                "emailAddresses": []map[string]interface{}{
                        {
                                "metadata": map[string]interface{}{"verified": true},
@@ -552,7 +445,7 @@ func (s *OIDCLoginSuite) TestGoogleLogin_NoPrimaryEmailAddress(c *check.C) {
        }
        state := s.startLogin(c)
        s.localdb.Login(context.Background(), arvados.LoginOptions{
-               Code:  s.validCode,
+               Code:  s.fakeProvider.ValidCode,
                State: state,
        })
 
@@ -574,23 +467,6 @@ func (s *OIDCLoginSuite) startLogin(c *check.C) (state string) {
        return
 }
 
-func (s *OIDCLoginSuite) fakeToken(c *check.C, payload []byte) string {
-       signer, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.RS256, Key: s.issuerKey}, nil)
-       if err != nil {
-               c.Error(err)
-       }
-       object, err := signer.Sign(payload)
-       if err != nil {
-               c.Error(err)
-       }
-       t, err := object.CompactSerialize()
-       if err != nil {
-               c.Error(err)
-       }
-       c.Logf("fakeToken(%q) == %q", payload, t)
-       return t
-}
-
 func getCallbackAuthInfo(c *check.C, railsSpy *arvadostest.Proxy) (authinfo rpc.UserSessionAuthInfo) {
        for _, dump := range railsSpy.RequestDumps {
                c.Logf("spied request: %q", dump)
index 1440836547253bc96871c651e666ec8608af243a..9ed758c0a474767e67e529afac94aef5c22d2a79 100644 (file)
@@ -116,9 +116,8 @@ Collection <- R6::R6Class(
 
                     private$REST$create(file, self$uuid)
                     newTreeBranch$setCollection(self)
+                   newTreeBranch
                 })
-
-                "Created"
             }
             else
             {
index 63bf55373d17a163522d1f2fdcf0ee4d640361d5..8cc89d902051a9ac752bf354a7b476cb344b60fc 100644 (file)
@@ -71,6 +71,12 @@ arv$setNumRetries(5)
 collection <- arv$collections.get("uuid")
 ```
 
+Be aware that the result from `collections.get` is _not_ a
+`Collection` class.  The object returned from this method lets you
+access collection fields like "name" and "description".  The
+`Collection` class lets you access the files in the collection for
+reading and writing, and is described in the next section.
+
 * List collections:
 
 ```{r}
@@ -78,9 +84,7 @@ collection <- arv$collections.get("uuid")
 collectionList <- arv$collections.list(list(list("name", "like", "Test%")))
 
 collectionList <- arv$collections.list(list(list("name", "like", "Test%")), limit = 10, offset = 2)
-```
 
-```{r}
 # count of total number of items (may be more than returned due to paging)
 collectionList$items_available
 
@@ -106,7 +110,7 @@ deletedCollection <- arv$collections.delete("uuid")
 updatedCollection <- arv$collections.update(list(name = "New name", description = "New description"), "uuid")
 ```
 
-* Create collection:
+* Create a new collection:
 
 ```{r}
 newCollection <- arv$collections.create(list(name = "Example", description = "This is a test collection"))
@@ -115,7 +119,7 @@ newCollection <- arv$collections.create(list(name = "Example", description = "Th
 
 #### Manipulating collection content
 
-* Create collection object:
+* Initialize a collection object:
 
 ```{r}
 collection <- Collection$new(arv, "uuid")
@@ -150,13 +154,13 @@ mytable       <- read.table(arvConnection)
 * Write a table:
 
 ```{r}
-arvadosFile   <- collection$create("myoutput.txt")
+arvadosFile   <- collection$create("myoutput.txt")[[1]]
 arvConnection <- arvadosFile$connection("w")
 write.table(mytable, arvConnection)
 arvadosFile$flush()
 ```
 
-* Write to existing file (override current content of the file):
+* Write to existing file (overwrites current content of the file):
 
 ```{r}
 arvadosFile <- collection$get("location/to/my/file.cpp")
@@ -183,7 +187,7 @@ or
 size <- arvadosSubcollection$getSizeInBytes()
 ```
 
-* Create new file in a collection:
+* Create new file in a collection (returns a vector of one or more ArvadosFile objects):
 
 ```{r}
 collection$create(files)
@@ -192,7 +196,7 @@ collection$create(files)
 Example:
 
 ```{r}
-mainFile <- collection$create("cpp/src/main.cpp")
+mainFile <- collection$create("cpp/src/main.cpp")[[1]]
 fileList <- collection$create(c("cpp/src/main.cpp", "cpp/src/util.h"))
 ```
 
index 4096a2eb156b39bc26a94a428342dbd77815f56a..5e19d6b2cbef77a2a5b223b83e9321be35e78600 100644 (file)
@@ -18,6 +18,7 @@ begin
   else
     version = `#{__dir__}/../../build/version-at-commit.sh #{git_hash}`.encode('utf-8').strip
   end
+  version = version.sub("~dev", ".dev").sub("~rc", ".rc")
   git_timestamp = Time.at(git_timestamp.to_i).utc
 ensure
   ENV["GIT_DIR"] = git_dir
index d9ce12487a1ce3c4f281296125e8cf81bc6b48fe..d5f48c066bd485262b0226e60b09a7d1d857a520 100644 (file)
@@ -30,12 +30,13 @@ def git_version_at_commit():
     return myversion
 
 def save_version(setup_dir, module, v):
-  with open(os.path.join(setup_dir, module, "_version.py"), 'wt') as fp:
-      return fp.write("__version__ = '%s'\n" % v)
+    v = v.replace("~dev", ".dev").replace("~rc", "rc")
+    with open(os.path.join(setup_dir, module, "_version.py"), 'wt') as fp:
+        return fp.write("__version__ = '%s'\n" % v)
 
 def read_version(setup_dir, module):
-  with open(os.path.join(setup_dir, module, "_version.py"), 'rt') as fp:
-      return re.match("__version__ = '(.*)'$", fp.read()).groups()[0]
+    with open(os.path.join(setup_dir, module, "_version.py"), 'rt') as fp:
+        return re.match("__version__ = '(.*)'$", fp.read()).groups()[0]
 
 def get_version(setup_dir, module):
     env_version = os.environ.get("ARVADOS_BUILDING_VERSION")
index dc4a6263178afdff11cbdd296c56f5b846467c3f..935bec63b6d6a0b585c0eca38a22e20a42d304a7 100755 (executable)
@@ -142,6 +142,8 @@ else
   TMPHERE=\$(pwd)
   cd /usr/src/arvados
   calculate_python_sdk_cwl_package_versions
+
+  cwl_runner_version=\$(echo -n \$cwl_runner_version | sed s/~dev/.dev/g | sed s/~rc/rc/g)
   cd \$TMPHERE
   set -u
 
index abd97ed8f46bf0cf6f864ad7905221835d4efbc7..0005b36572a6b8e4b85b4b62e72629cafdcf765c 100644 (file)
@@ -44,6 +44,7 @@ requirements:
           r["Clusters"][inputs.this_cluster_id] = {"RemoteClusters": remoteClusters};
           if (r["Clusters"][inputs.this_cluster_id]) {
             r["Clusters"][inputs.this_cluster_id]["Login"] = {"LoginCluster": inputs.cluster_ids[0]};
+            r["Clusters"][inputs.this_cluster_id]["Users"] = {"AutoAdminFirstUser": false};
           }
           return JSON.stringify(r);
           }
diff --git a/sdk/go/arvadostest/oidc_provider.go b/sdk/go/arvadostest/oidc_provider.go
new file mode 100644 (file)
index 0000000..96205f9
--- /dev/null
@@ -0,0 +1,174 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: Apache-2.0
+
+package arvadostest
+
+import (
+       "crypto/rand"
+       "crypto/rsa"
+       "encoding/base64"
+       "encoding/json"
+       "net/http"
+       "net/http/httptest"
+       "net/url"
+       "strings"
+       "time"
+
+       "gopkg.in/check.v1"
+       "gopkg.in/square/go-jose.v2"
+)
+
+type OIDCProvider struct {
+       // expected token request
+       ValidCode         string
+       ValidClientID     string
+       ValidClientSecret string
+       // desired response from token endpoint
+       AuthEmail         string
+       AuthEmailVerified bool
+       AuthName          string
+
+       PeopleAPIResponse map[string]interface{}
+
+       key       *rsa.PrivateKey
+       Issuer    *httptest.Server
+       PeopleAPI *httptest.Server
+       c         *check.C
+}
+
+func NewOIDCProvider(c *check.C) *OIDCProvider {
+       p := &OIDCProvider{c: c}
+       var err error
+       p.key, err = rsa.GenerateKey(rand.Reader, 2048)
+       c.Assert(err, check.IsNil)
+       p.Issuer = httptest.NewServer(http.HandlerFunc(p.serveOIDC))
+       p.PeopleAPI = httptest.NewServer(http.HandlerFunc(p.servePeopleAPI))
+       return p
+}
+
+func (p *OIDCProvider) ValidAccessToken() string {
+       return p.fakeToken([]byte("fake access token"))
+}
+
+func (p *OIDCProvider) serveOIDC(w http.ResponseWriter, req *http.Request) {
+       req.ParseForm()
+       p.c.Logf("serveOIDC: got req: %s %s %s", req.Method, req.URL, req.Form)
+       w.Header().Set("Content-Type", "application/json")
+       switch req.URL.Path {
+       case "/.well-known/openid-configuration":
+               json.NewEncoder(w).Encode(map[string]interface{}{
+                       "issuer":                 p.Issuer.URL,
+                       "authorization_endpoint": p.Issuer.URL + "/auth",
+                       "token_endpoint":         p.Issuer.URL + "/token",
+                       "jwks_uri":               p.Issuer.URL + "/jwks",
+                       "userinfo_endpoint":      p.Issuer.URL + "/userinfo",
+               })
+       case "/token":
+               var clientID, clientSecret string
+               auth, _ := base64.StdEncoding.DecodeString(strings.TrimPrefix(req.Header.Get("Authorization"), "Basic "))
+               authsplit := strings.Split(string(auth), ":")
+               if len(authsplit) == 2 {
+                       clientID, _ = url.QueryUnescape(authsplit[0])
+                       clientSecret, _ = url.QueryUnescape(authsplit[1])
+               }
+               if clientID != p.ValidClientID || clientSecret != p.ValidClientSecret {
+                       p.c.Logf("OIDCProvider: expected (%q, %q) got (%q, %q)", p.ValidClientID, p.ValidClientSecret, clientID, clientSecret)
+                       w.WriteHeader(http.StatusUnauthorized)
+                       return
+               }
+
+               if req.Form.Get("code") != p.ValidCode || p.ValidCode == "" {
+                       w.WriteHeader(http.StatusUnauthorized)
+                       return
+               }
+               idToken, _ := json.Marshal(map[string]interface{}{
+                       "iss":            p.Issuer.URL,
+                       "aud":            []string{clientID},
+                       "sub":            "fake-user-id",
+                       "exp":            time.Now().UTC().Add(time.Minute).Unix(),
+                       "iat":            time.Now().UTC().Unix(),
+                       "nonce":          "fake-nonce",
+                       "email":          p.AuthEmail,
+                       "email_verified": p.AuthEmailVerified,
+                       "name":           p.AuthName,
+                       "alt_verified":   true,                    // for custom claim tests
+                       "alt_email":      "alt_email@example.com", // for custom claim tests
+                       "alt_username":   "desired-username",      // for custom claim tests
+               })
+               json.NewEncoder(w).Encode(struct {
+                       AccessToken  string `json:"access_token"`
+                       TokenType    string `json:"token_type"`
+                       RefreshToken string `json:"refresh_token"`
+                       ExpiresIn    int32  `json:"expires_in"`
+                       IDToken      string `json:"id_token"`
+               }{
+                       AccessToken:  p.ValidAccessToken(),
+                       TokenType:    "Bearer",
+                       RefreshToken: "test-refresh-token",
+                       ExpiresIn:    30,
+                       IDToken:      p.fakeToken(idToken),
+               })
+       case "/jwks":
+               json.NewEncoder(w).Encode(jose.JSONWebKeySet{
+                       Keys: []jose.JSONWebKey{
+                               {Key: p.key.Public(), Algorithm: string(jose.RS256), KeyID: ""},
+                       },
+               })
+       case "/auth":
+               w.WriteHeader(http.StatusInternalServerError)
+       case "/userinfo":
+               if authhdr := req.Header.Get("Authorization"); strings.TrimPrefix(authhdr, "Bearer ") != p.ValidAccessToken() {
+                       p.c.Logf("OIDCProvider: bad auth %q", authhdr)
+                       w.WriteHeader(http.StatusUnauthorized)
+                       return
+               }
+               json.NewEncoder(w).Encode(map[string]interface{}{
+                       "sub":            "fake-user-id",
+                       "name":           p.AuthName,
+                       "given_name":     p.AuthName,
+                       "family_name":    "",
+                       "alt_username":   "desired-username",
+                       "email":          p.AuthEmail,
+                       "email_verified": p.AuthEmailVerified,
+               })
+       default:
+               w.WriteHeader(http.StatusNotFound)
+       }
+}
+
+func (p *OIDCProvider) servePeopleAPI(w http.ResponseWriter, req *http.Request) {
+       req.ParseForm()
+       p.c.Logf("servePeopleAPI: got req: %s %s %s", req.Method, req.URL, req.Form)
+       w.Header().Set("Content-Type", "application/json")
+       switch req.URL.Path {
+       case "/v1/people/me":
+               if f := req.Form.Get("personFields"); f != "emailAddresses,names" {
+                       w.WriteHeader(http.StatusBadRequest)
+                       break
+               }
+               json.NewEncoder(w).Encode(p.PeopleAPIResponse)
+       default:
+               w.WriteHeader(http.StatusNotFound)
+       }
+}
+
+func (p *OIDCProvider) fakeToken(payload []byte) string {
+       signer, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.RS256, Key: p.key}, nil)
+       if err != nil {
+               p.c.Error(err)
+               return ""
+       }
+       object, err := signer.Sign(payload)
+       if err != nil {
+               p.c.Error(err)
+               return ""
+       }
+       t, err := object.CompactSerialize()
+       if err != nil {
+               p.c.Error(err)
+               return ""
+       }
+       p.c.Logf("fakeToken(%q) == %q", payload, t)
+       return t
+}
index b6a85e05e786fa1d0ace1715eab1cacdc3e7d0cc..f1c2e243b53a8f5d7ae604d1b67df55968430fcd 100644 (file)
@@ -97,7 +97,7 @@ func (a *Credentials) loadTokenFromCookie(r *http.Request) {
        a.Tokens = append(a.Tokens, string(token))
 }
 
-// LoadTokensFromHTTPRequestBody() loads credentials from the request
+// LoadTokensFromHTTPRequestBody loads credentials from the request
 // body.
 //
 // This is separate from LoadTokensFromHTTPRequest() because it's not
index 52aa45f858746a7da6f79b57a5a6c6f32c044f45..ecb09964ecc50585a3c213a8b3cb1f8642fb5050 100644 (file)
@@ -15,8 +15,8 @@ import (
 var LocatorPattern = regexp.MustCompile(
        "^[0-9a-fA-F]{32}\\+[0-9]+(\\+[A-Z][A-Za-z0-9@_-]*)*$")
 
-// Stores a Block Locator Digest compactly, up to 128 bits.
-// Can be used as a map key.
+// BlockDigest stores a Block Locator Digest compactly, up to 128 bits. Can be
+// used as a map key.
 type BlockDigest struct {
        H uint64
        L uint64
@@ -41,7 +41,7 @@ func (w DigestWithSize) String() string {
        return fmt.Sprintf("%s+%d", w.Digest.String(), w.Size)
 }
 
-// Will create a new BlockDigest unless an error is encountered.
+// FromString creates a new BlockDigest unless an error is encountered.
 func FromString(s string) (dig BlockDigest, err error) {
        if len(s) != 32 {
                err = fmt.Errorf("Block digest should be exactly 32 characters but this one is %d: %s", len(s), s)
index 7716a71b20a5311379e88f147467f51aed69d08b..6c7d3bf1e2acb4aa63d204ca5d35a71ce3d64220 100644 (file)
@@ -6,7 +6,7 @@
 
 package blockdigest
 
-// Just used for testing when we need some distinct BlockDigests
+// MakeTestBlockDigest is used for testing with distinct BlockDigests
 func MakeTestBlockDigest(i int) BlockDigest {
        return BlockDigest{L: uint64(i)}
 }
index 594379b4fabe6ed10383488e50be4d2b9e1a7de7..f4e99bdbe3ef8c699d1bd7aadf97a861f5950b1d 100644 (file)
@@ -18,7 +18,7 @@ import (
        "git.arvados.org/arvados.git/sdk/go/arvadosclient"
 )
 
-// Function used to emit debug messages. The easiest way to enable
+// DebugPrintf emits debug messages. The easiest way to enable
 // keepclient debug messages in your application is to assign
 // log.Printf to DebugPrintf.
 var DebugPrintf = func(string, ...interface{}) {}
index ae687c50bd98b62746afae0b3d381c59e5e42bd7..315fc74a713f42fbee7b7b030c36576ed5426bc0 100644 (file)
@@ -14,6 +14,7 @@ import logging
 import os
 import re
 import socket
+import sys
 import time
 import types
 
@@ -32,6 +33,9 @@ RETRY_DELAY_INITIAL = 2
 RETRY_DELAY_BACKOFF = 2
 RETRY_COUNT = 2
 
+if sys.version_info >= (3,):
+    httplib2.SSLHandshakeError = None
+
 class OrderedJsonModel(apiclient.model.JsonModel):
     """Model class for JSON that preserves the contents' order.
 
index 9aabff42929838a1f9748362a63eeed003775a64..36804bf5b36601e6e12b9bf1c86a979abc6dd40e 100644 (file)
@@ -15,12 +15,13 @@ def git_version_at_commit():
     return myversion
 
 def save_version(setup_dir, module, v):
-  with open(os.path.join(setup_dir, module, "_version.py"), 'wt') as fp:
-      return fp.write("__version__ = '%s'\n" % v)
+    v = v.replace("~dev", ".dev").replace("~rc", "rc")
+    with open(os.path.join(setup_dir, module, "_version.py"), 'wt') as fp:
+        return fp.write("__version__ = '%s'\n" % v)
 
 def read_version(setup_dir, module):
-  with open(os.path.join(setup_dir, module, "_version.py"), 'rt') as fp:
-      return re.match("__version__ = '(.*)'$", fp.read()).groups()[0]
+    with open(os.path.join(setup_dir, module, "_version.py"), 'rt') as fp:
+        return re.match("__version__ = '(.*)'$", fp.read()).groups()[0]
 
 def get_version(setup_dir, module):
     env_version = os.environ.get("ARVADOS_BUILDING_VERSION")
index 1591b7e17e1f519c2d92dc19514cc36d9ac1ed56..7b7c473e4b2635a14b98e85468a9dfe1651dfce9 100644 (file)
@@ -6,10 +6,10 @@ arv-federation-migrate should be in the path or the full path supplied
 in the 'fed_migrate' input parameter.
 
 # Create arvbox containers fedbox(1,2,3) for the federation
-$ cwltool arvbox-make-federation.cwl --arvbox_base ~/.arvbox > fed.json
+$ cwltool --preserve-environment=SSH_AUTH_SOCK arvbox-make-federation.cwl --arvbox_base ~/.arvbox > fed.json
 
 # Configure containers and run tests
-$ cwltool fed-migrate.cwl fed.json
+$ cwltool --preserve-environment=SSH_AUTH_SOCK fed-migrate.cwl fed.json
 
 CWL for running the test is generated using cwl-ex:
 
index 019e156a56d428ae79add12d720d58a7f24f7b3f..71db40c7fd6bc7f5eef8eb6f757e80466b6d3b9f 100644 (file)
@@ -18,6 +18,7 @@ begin
   else
     version = `#{__dir__}/../../build/version-at-commit.sh #{git_hash}`.encode('utf-8').strip
   end
+  version = version.sub("~dev", ".dev").sub("~rc", ".rc")
   git_timestamp = Time.at(git_timestamp.to_i).utc
 ensure
   ENV["GIT_DIR"] = git_dir
index 2644a06579787082d8e1c7421a5288a085450684..1800e125d27d8e0b89795ed836924762b75ec5d1 100644 (file)
@@ -182,7 +182,7 @@ class ApplicationController < ActionController::Base
       if params[pname].is_a?(Boolean)
         return params[pname]
       else
-        logger.warn "Warning: received non-boolean parameter '#{pname}' on #{self.class.inspect}."
+        logger.warn "Warning: received non-boolean value #{params[pname].inspect} for boolean parameter #{pname} on #{self.class.inspect}, treating as false."
       end
     end
     false
@@ -611,7 +611,7 @@ class ApplicationController < ActionController::Base
         # Make sure params[key] is either true or false -- not a
         # string, not nil, etc.
         if not params.include?(key)
-          params[key] = info[:default]
+          params[key] = info[:default] || false
         elsif [false, 'false', '0', 0].include? params[key]
           params[key] = false
         elsif [true, 'true', '1', 1].include? params[key]
index 656bd37ae6e3cf7e53bae89eac6e1b6086257d33..2e7e2f82b07c024168bd34080f000a91eaea55ac 100644 (file)
@@ -13,10 +13,10 @@ class Arvados::V1::CollectionsController < ApplicationController
     (super rescue {}).
       merge({
         include_trash: {
-          type: 'boolean', required: false, description: "Include collections whose is_trashed attribute is true."
+          type: 'boolean', required: false, default: false, description: "Include collections whose is_trashed attribute is true.",
         },
         include_old_versions: {
-          type: 'boolean', required: false, description: "Include past collection versions."
+          type: 'boolean', required: false, default: false, description: "Include past collection versions.",
         },
       })
   end
@@ -25,10 +25,10 @@ class Arvados::V1::CollectionsController < ApplicationController
     (super rescue {}).
       merge({
         include_trash: {
-          type: 'boolean', required: false, description: "Show collection even if its is_trashed attribute is true."
+          type: 'boolean', required: false, default: false, description: "Show collection even if its is_trashed attribute is true.",
         },
         include_old_versions: {
-          type: 'boolean', required: false, description: "Include past collection versions."
+          type: 'boolean', required: false, default: true, description: "Include past collection versions.",
         },
       })
   end
@@ -44,30 +44,22 @@ class Arvados::V1::CollectionsController < ApplicationController
   end
 
   def find_objects_for_index
-    opts = {}
-    if params[:include_trash] || ['destroy', 'trash', 'untrash'].include?(action_name)
-      opts.update({include_trash: true})
-    end
-    if params[:include_old_versions] || @include_old_versions
-      opts.update({include_old_versions: true})
-    end
+    opts = {
+      include_trash: params[:include_trash] || ['destroy', 'trash', 'untrash'].include?(action_name),
+      include_old_versions: params[:include_old_versions] || false,
+    }
     @objects = Collection.readable_by(*@read_users, opts) if !opts.empty?
     super
   end
 
   def find_object_by_uuid
-    if params[:include_old_versions].nil?
-      @include_old_versions = true
-    else
-      @include_old_versions = params[:include_old_versions]
-    end
-
     if loc = Keep::Locator.parse(params[:id])
       loc.strip_hints!
 
-      opts = {}
-      opts.update({include_trash: true}) if params[:include_trash]
-      opts.update({include_old_versions: @include_old_versions})
+      opts = {
+        include_trash: params[:include_trash],
+        include_old_versions: params[:include_old_versions],
+      }
 
       # It matters which Collection object we pick because we use it to get signed_manifest_text,
       # the value of which is affected by the value of trash_at.
index 3d5d4616ef0ace8783357c4f041c1a491cbd6615..07b8098f5ba8749c7aff875ccf78347e745ecef4 100644 (file)
@@ -15,7 +15,7 @@ class Arvados::V1::ContainerRequestsController < ApplicationController
     (super rescue {}).
       merge({
         include_trash: {
-          type: 'boolean', required: false, description: "Include container requests whose owner project is trashed."
+          type: 'boolean', required: false, default: false, description: "Include container requests whose owner project is trashed.",
         },
       })
   end
@@ -24,7 +24,7 @@ class Arvados::V1::ContainerRequestsController < ApplicationController
     (super rescue {}).
       merge({
         include_trash: {
-          type: 'boolean', required: false, description: "Show container request even if its owner project is trashed."
+          type: 'boolean', required: false, default: false, description: "Show container request even if its owner project is trashed.",
         },
       })
   end
index 46d3a75a3a24407ac8ecb1541f2e646b89daf946..812929d500c408873472cfce1ac4261828e8f197 100644 (file)
@@ -14,7 +14,7 @@ class Arvados::V1::GroupsController < ApplicationController
     (super rescue {}).
       merge({
         include_trash: {
-          type: 'boolean', required: false, description: "Include items whose is_trashed attribute is true."
+          type: 'boolean', required: false, default: false, description: "Include items whose is_trashed attribute is true.",
         },
       })
   end
@@ -23,7 +23,7 @@ class Arvados::V1::GroupsController < ApplicationController
     (super rescue {}).
       merge({
         include_trash: {
-          type: 'boolean', required: false, description: "Show group/project even if its is_trashed attribute is true."
+          type: 'boolean', required: false, default: false, description: "Show group/project even if its is_trashed attribute is true.",
         },
       })
   end
@@ -32,13 +32,16 @@ class Arvados::V1::GroupsController < ApplicationController
     params = _index_requires_parameters.
       merge({
               uuid: {
-                type: 'string', required: false, default: nil
+                type: 'string', required: false, default: nil,
               },
               recursive: {
-                type: 'boolean', required: false, description: 'Include contents from child groups recursively.'
+                type: 'boolean', required: false, default: false, description: 'Include contents from child groups recursively.',
               },
               include: {
-                type: 'string', required: false, description: 'Include objects referred to by listed field in "included" (only owner_uuid)'
+                type: 'string', required: false, description: 'Include objects referred to by listed field in "included" (only owner_uuid).',
+              },
+              include_old_versions: {
+                type: 'boolean', required: false, default: false, description: 'Include past collection versions.',
               }
             })
     params.delete(:select)
@@ -53,7 +56,7 @@ class Arvados::V1::GroupsController < ApplicationController
           type: 'boolean',
           location: 'query',
           default: false,
-          description: 'defer permissions update'
+          description: 'defer permissions update',
         }
       }
     )
@@ -67,7 +70,7 @@ class Arvados::V1::GroupsController < ApplicationController
           type: 'boolean',
           location: 'query',
           default: false,
-          description: 'defer permissions update'
+          description: 'defer permissions update',
         }
       }
     )
@@ -283,8 +286,10 @@ class Arvados::V1::GroupsController < ApplicationController
         end
       end.compact
 
-      @objects = klass.readable_by(*@read_users, {:include_trash => params[:include_trash]}).
-                 order(request_order).where(where_conds)
+      @objects = klass.readable_by(*@read_users, {
+          :include_trash => params[:include_trash],
+          :include_old_versions => params[:include_old_versions]
+        }).order(request_order).where(where_conds)
 
       if params['exclude_home_project']
         @objects = exclude_home @objects, klass
index 58a3fd168deed0d5615973fb1578cb619ed13abc..2d6b05269dd12bfe221bbd7848e926f6389dd364 100644 (file)
@@ -40,16 +40,16 @@ class Arvados::V1::JobsController < ApplicationController
     (super rescue {}).
       merge({
               find_or_create: {
-                type: 'boolean', required: false, default: false
+                type: 'boolean', required: false, default: false,
               },
               filters: {
-                type: 'array', required: false
+                type: 'array', required: false,
               },
               minimum_script_version: {
-                type: 'string', required: false
+                type: 'string', required: false,
               },
               exclude_script_versions: {
-                type: 'array', required: false
+                type: 'array', required: false,
               },
             })
   end
index 76e8da0c72b306c7fc33612f5c345d866624c1fa..f4d42edf6c1891e69e644d4a0d0c86cd952a0aa1 100644 (file)
@@ -132,16 +132,8 @@ class Arvados::V1::UsersController < ApplicationController
     end
 
     @response = @object.setup(repo_name: full_repo_name,
-                              vm_uuid: params[:vm_uuid])
-
-    # setup succeeded. send email to user
-    if params[:send_notification_email] && !Rails.configuration.Users.UserSetupMailText.empty?
-      begin
-        UserNotifier.account_is_setup(@object).deliver_now
-      rescue => e
-        logger.warn "Failed to send email to #{@object.email}: #{e}"
-      end
-    end
+                              vm_uuid: params[:vm_uuid],
+                              send_notification_email: params[:send_notification_email])
 
     send_json kind: "arvados#HashList", items: @response.as_api_response(nil)
   end
@@ -230,7 +222,7 @@ class Arvados::V1::UsersController < ApplicationController
         type: 'string', required: false,
       },
       redirect_to_new_user: {
-        type: 'boolean', required: false,
+        type: 'boolean', required: false, default: false,
       },
       old_user_uuid: {
         type: 'string', required: false,
@@ -244,19 +236,19 @@ class Arvados::V1::UsersController < ApplicationController
   def self._setup_requires_parameters
     {
       uuid: {
-        type: 'string', required: false
+        type: 'string', required: false,
       },
       user: {
-        type: 'object', required: false
+        type: 'object', required: false,
       },
       repo_name: {
-        type: 'string', required: false
+        type: 'string', required: false,
       },
       vm_uuid: {
-        type: 'string', required: false
+        type: 'string', required: false,
       },
       send_notification_email: {
-        type: 'boolean', required: false, default: false
+        type: 'boolean', required: false, default: false,
       },
     }
   end
@@ -264,7 +256,7 @@ class Arvados::V1::UsersController < ApplicationController
   def self._update_requires_parameters
     super.merge({
       bypass_federation: {
-        type: 'boolean', required: false,
+        type: 'boolean', required: false, default: false,
       },
     })
   end
index acdc4858118fcb4c3fd5be1a1a65208ed72ff530..be4e8bb0b6a1f11e02f74739b7832bc2013e6869 100644 (file)
@@ -43,7 +43,7 @@ class ArvadosApiToken
     auth = nil
     [params["api_token"],
      params["oauth_token"],
-     env["HTTP_AUTHORIZATION"].andand.match(/(OAuth2|Bearer) ([-\/a-zA-Z0-9]+)/).andand[2],
+     env["HTTP_AUTHORIZATION"].andand.match(/(OAuth2|Bearer) ([!-~]+)/).andand[2],
      *reader_tokens,
     ].each do |supplied|
       next if !supplied
index 518fe569377ab1b2c2b21880e4f3411aa340844a..74a4c1efa571b5229825243c98034957e99d4e45 100644 (file)
@@ -128,6 +128,10 @@ class ApiClientAuthorization < ArvadosModel
       return auth
     end
 
+    token_uuid = ''
+    secret = token
+    optional = nil
+
     case token[0..2]
     when 'v2/'
       _, token_uuid, secret, optional = token.split('/')
@@ -170,94 +174,109 @@ class ApiClientAuthorization < ArvadosModel
         return auth
       end
 
-      token_uuid_prefix = token_uuid[0..4]
-      if token_uuid_prefix == Rails.configuration.ClusterID
+      upstream_cluster_id = token_uuid[0..4]
+      if upstream_cluster_id == Rails.configuration.ClusterID
         # Token is supposedly issued by local cluster, but if the
         # token were valid, we would have been found in the database
         # in the above query.
         return nil
-      elsif token_uuid_prefix.length != 5
+      elsif upstream_cluster_id.length != 5
         # malformed
         return nil
       end
 
-      # Invariant: token_uuid_prefix != Rails.configuration.ClusterID
-      #
-      # In other words the remaing code in this method below is the
-      # case that determines whether to accept a token that was issued
-      # by a remote cluster when the token absent or expired in our
-      # database.  To begin, we need to ask the cluster that issued
-      # the token to [re]validate it.
-      clnt = ApiClientAuthorization.make_http_client(uuid_prefix: token_uuid_prefix)
-
-      host = remote_host(uuid_prefix: token_uuid_prefix)
-      if !host
-        Rails.logger.warn "remote authentication rejected: no host for #{token_uuid_prefix.inspect}"
+    else
+      # token is not a 'v2' token. It could be just the secret part
+      # ("v1 token") -- or it could be an OpenIDConnect access token,
+      # in which case either (a) the controller will have inserted a
+      # row with api_token = hmac(systemroottoken,oidctoken) before
+      # forwarding it, or (b) we'll have done that ourselves, or (c)
+      # we'll need to ask LoginCluster to validate it for us below,
+      # and then insert a local row for a faster lookup next time.
+      hmac = OpenSSL::HMAC.hexdigest('sha256', Rails.configuration.SystemRootToken, token)
+      auth = ApiClientAuthorization.
+               includes(:user, :api_client).
+               where('api_token in (?, ?) and (expires_at is null or expires_at > CURRENT_TIMESTAMP)', token, hmac).
+               first
+      if auth && auth.user
+        return auth
+      elsif !Rails.configuration.Login.LoginCluster.blank? && Rails.configuration.Login.LoginCluster != Rails.configuration.ClusterID
+        # An unrecognized non-v2 token might be an OIDC Access Token
+        # that can be verified by our login cluster in the code
+        # below. If so, we'll stuff the database with hmac instead of
+        # the real OIDC token.
+        upstream_cluster_id = Rails.configuration.Login.LoginCluster
+        token_uuid = upstream_cluster_id + generate_uuid[5..27]
+        secret = hmac
+      else
         return nil
       end
+    end
 
-      begin
-        remote_user = SafeJSON.load(
-          clnt.get_content('https://' + host + '/arvados/v1/users/current',
-                           {'remote' => Rails.configuration.ClusterID},
-                           {'Authorization' => 'Bearer ' + token}))
-      rescue => e
-        Rails.logger.warn "remote authentication with token #{token.inspect} failed: #{e}"
-        return nil
-      end
+    # Invariant: upstream_cluster_id != Rails.configuration.ClusterID
+    #
+    # In other words the remaining code in this method decides
+    # whether to accept a token that was issued by a remote cluster
+    # when the token is absent or expired in our database.  To
+    # begin, we need to ask the cluster that issued the token to
+    # [re]validate it.
+    clnt = ApiClientAuthorization.make_http_client(uuid_prefix: upstream_cluster_id)
+
+    host = remote_host(uuid_prefix: upstream_cluster_id)
+    if !host
+      Rails.logger.warn "remote authentication rejected: no host for #{upstream_cluster_id.inspect}"
+      return nil
+    end
 
-      # Check the response is well formed.
-      if !remote_user.is_a?(Hash) || !remote_user['uuid'].is_a?(String)
-        Rails.logger.warn "remote authentication rejected: remote_user=#{remote_user.inspect}"
-        return nil
-      end
+    begin
+      remote_user = SafeJSON.load(
+        clnt.get_content('https://' + host + '/arvados/v1/users/current',
+                         {'remote' => Rails.configuration.ClusterID},
+                         {'Authorization' => 'Bearer ' + token}))
+    rescue => e
+      Rails.logger.warn "remote authentication with token #{token.inspect} failed: #{e}"
+      return nil
+    end
 
-      remote_user_prefix = remote_user['uuid'][0..4]
+    # Check the response is well formed.
+    if !remote_user.is_a?(Hash) || !remote_user['uuid'].is_a?(String)
+      Rails.logger.warn "remote authentication rejected: remote_user=#{remote_user.inspect}"
+      return nil
+    end
 
-      # Clusters can only authenticate for their own users.
-      if remote_user_prefix != token_uuid_prefix
-        Rails.logger.warn "remote authentication rejected: claimed remote user #{remote_user_prefix} but token was issued by #{token_uuid_prefix}"
-        return nil
-      end
+    remote_user_prefix = remote_user['uuid'][0..4]
 
-      # Invariant:    remote_user_prefix == token_uuid_prefix
-      # therefore:    remote_user_prefix != Rails.configuration.ClusterID
+    # Clusters can only authenticate for their own users.
+    if remote_user_prefix != upstream_cluster_id
+      Rails.logger.warn "remote authentication rejected: claimed remote user #{remote_user_prefix} but token was issued by #{upstream_cluster_id}"
+      return nil
+    end
 
-      # Add or update user and token in local database so we can
-      # validate subsequent requests faster.
+    # Invariant:    remote_user_prefix == upstream_cluster_id
+    # therefore:    remote_user_prefix != Rails.configuration.ClusterID
 
-      if remote_user['uuid'][-22..-1] == '-tpzed-anonymouspublic'
-        # Special case: map the remote anonymous user to local anonymous user
-        remote_user['uuid'] = anonymous_user_uuid
-      end
+    # Add or update user and token in local database so we can
+    # validate subsequent requests faster.
 
-      user = User.find_by_uuid(remote_user['uuid'])
+    if remote_user['uuid'][-22..-1] == '-tpzed-anonymouspublic'
+      # Special case: map the remote anonymous user to local anonymous user
+      remote_user['uuid'] = anonymous_user_uuid
+    end
 
-      if !user
-        # Create a new record for this user.
-        user = User.new(uuid: remote_user['uuid'],
-                        is_active: false,
-                        is_admin: false,
-                        email: remote_user['email'],
-                        owner_uuid: system_user_uuid)
-        user.set_initial_username(requested: remote_user['username'])
-      end
+    user = User.find_by_uuid(remote_user['uuid'])
 
-      # Sync user record.
-      if remote_user_prefix == Rails.configuration.Login.LoginCluster
-        # Remote cluster controls our user database, set is_active if
-        # remote is active.  If remote is not active, user will be
-        # unsetup (see below).
-        user.is_active = true if remote_user['is_active']
-        user.is_admin = remote_user['is_admin']
-      else
-        if Rails.configuration.Users.NewUsersAreActive ||
-           Rails.configuration.RemoteClusters[remote_user_prefix].andand["ActivateUsers"]
-          # Default policy is to activate users
-          user.is_active = true if remote_user['is_active']
-        end
-      end
+    if !user
+      # Create a new record for this user.
+      user = User.new(uuid: remote_user['uuid'],
+                      is_active: false,
+                      is_admin: false,
+                      email: remote_user['email'],
+                      owner_uuid: system_user_uuid)
+      user.set_initial_username(requested: remote_user['username'])
+    end
 
+    # Sync user record.
+    act_as_system_user do
       %w[first_name last_name email prefs].each do |attr|
         user.send(attr+'=', remote_user[attr])
       end
@@ -267,40 +286,54 @@ class ApiClientAuthorization < ArvadosModel
         user.last_name = "from cluster #{remote_user_prefix}"
       end
 
-      act_as_system_user do
-        if (user.is_active && !remote_user['is_active']) or (user.is_invited && !remote_user['is_invited'])
-          # Synchronize the user's "active/invited" state state.  This
-          # also saves the record.
-          user.unsetup
-        else
-          user.save!
+      user.save!
+
+      if user.is_invited && !remote_user['is_invited']
+        # Remote user is not "invited" state, they should be unsetup, which
+        # also makes them inactive.
+        user.unsetup
+      else
+        if !user.is_invited && remote_user['is_invited'] and
+          (remote_user_prefix == Rails.configuration.Login.LoginCluster or
+           Rails.configuration.Users.AutoSetupNewUsers or
+           Rails.configuration.Users.NewUsersAreActive or
+           Rails.configuration.RemoteClusters[remote_user_prefix].andand["ActivateUsers"])
+          user.setup
         end
 
-        # We will accept this token (and avoid reloading the user
-        # record) for 'RemoteTokenRefresh' (default 5 minutes).
-        # Possible todo:
-        # Request the actual api_client_auth record from the remote
-        # server in case it wants the token to expire sooner.
-        auth = ApiClientAuthorization.find_or_create_by(uuid: token_uuid) do |auth|
-          auth.user = user
-          auth.api_client_id = 0
+        if !user.is_active && remote_user['is_active'] && user.is_invited and
+          (remote_user_prefix == Rails.configuration.Login.LoginCluster or
+           Rails.configuration.Users.NewUsersAreActive or
+           Rails.configuration.RemoteClusters[remote_user_prefix].andand["ActivateUsers"])
+          user.update_attributes!(is_active: true)
+        elsif user.is_active && !remote_user['is_active']
+          user.update_attributes!(is_active: false)
+        end
+
+        if remote_user_prefix == Rails.configuration.Login.LoginCluster and
+          user.is_active and
+          user.is_admin != remote_user['is_admin']
+          # Remote cluster controls our user database, including the
+          # admin flag.
+          user.update_attributes!(is_admin: remote_user['is_admin'])
         end
-        auth.update_attributes!(user: user,
-                                api_token: secret,
-                                api_client_id: 0,
-                                expires_at: Time.now + Rails.configuration.Login.RemoteTokenRefresh)
-        Rails.logger.debug "cached remote token #{token_uuid} with secret #{secret} in local db"
       end
-      return auth
-    else
-      # token is not a 'v2' token
-      auth = ApiClientAuthorization.
-               includes(:user, :api_client).
-               where('api_token=? and (expires_at is null or expires_at > CURRENT_TIMESTAMP)', token).
-               first
-      if auth && auth.user
-        return auth
+
+      # We will accept this token (and avoid reloading the user
+      # record) for 'RemoteTokenRefresh' (default 5 minutes).
+      # Possible todo:
+      # Request the actual api_client_auth record from the remote
+      # server in case it wants the token to expire sooner.
+      auth = ApiClientAuthorization.find_or_create_by(uuid: token_uuid) do |auth|
+        auth.user = user
+        auth.api_client_id = 0
       end
+      auth.update_attributes!(user: user,
+                              api_token: secret,
+                              api_client_id: 0,
+                              expires_at: Time.now + Rails.configuration.Login.RemoteTokenRefresh)
+      Rails.logger.debug "cached remote token #{token_uuid} with secret #{secret} in local db"
+      return auth
     end
 
     return nil
index 8ec90f7e53a38805eff5b9ebac846eb88a4d7117..6f30b27a9595e1bfd2958e6554234cc787415fc8 100644 (file)
@@ -26,7 +26,7 @@ class User < ArvadosModel
   before_update :verify_repositories_empty, :if => Proc.new {
     username.nil? and username_changed?
   }
-  before_update :setup_on_activate
+  after_update :setup_on_activate
 
   before_create :check_auto_admin
   before_create :set_initial_username, :if => Proc.new {
@@ -213,11 +213,44 @@ SELECT target_uuid, perm_level
   end
 
   # create links
-  def setup(repo_name: nil, vm_uuid: nil)
-    repo_perm = create_user_repo_link repo_name
-    vm_login_perm = create_vm_login_permission_link(vm_uuid, username) if vm_uuid
+  def setup(repo_name: nil, vm_uuid: nil, send_notification_email: nil)
+    newly_invited = Link.where(tail_uuid: self.uuid,
+                              head_uuid: all_users_group_uuid,
+                              link_class: 'permission',
+                              name: 'can_read').empty?
+
+    # Add can_read link from this user to "all users" which makes this
+    # user "invited"
     group_perm = create_user_group_link
 
+    # Add git repo
+    repo_perm = if (!repo_name.nil? || Rails.configuration.Users.AutoSetupNewUsersWithRepository) and !username.nil?
+                  repo_name ||= "#{username}/#{username}"
+                  create_user_repo_link repo_name
+                end
+
+    # Add virtual machine
+    if vm_uuid.nil? and !Rails.configuration.Users.AutoSetupNewUsersWithVmUUID.empty?
+      vm_uuid = Rails.configuration.Users.AutoSetupNewUsersWithVmUUID
+    end
+
+    vm_login_perm = if vm_uuid && username
+                      create_vm_login_permission_link(vm_uuid, username)
+                    end
+
+    # Send welcome email
+    if send_notification_email.nil?
+      send_notification_email = Rails.configuration.Mail.SendUserSetupNotificationEmail
+    end
+
+    if newly_invited and send_notification_email and !Rails.configuration.Users.UserSetupMailText.empty?
+      begin
+        UserNotifier.account_is_setup(self).deliver_now
+      rescue => e
+        logger.warn "Failed to send email to #{self.email}: #{e}"
+      end
+    end
+
     return [repo_perm, vm_login_perm, group_perm, self].compact
   end
 
@@ -255,6 +288,7 @@ SELECT target_uuid, perm_level
     self.prefs = {}
 
     # mark the user as inactive
+    self.is_admin = false  # can't be admin and inactive
     self.is_active = false
     self.save!
   end
@@ -746,17 +780,6 @@ update #{PERMISSION_VIEW} set target_uuid=$1 where target_uuid = $2
   # Automatically setup new user during creation
   def auto_setup_new_user
     setup
-    if username
-      create_vm_login_permission_link(Rails.configuration.Users.AutoSetupNewUsersWithVmUUID,
-                                      username)
-      repo_name = "#{username}/#{username}"
-      if Rails.configuration.Users.AutoSetupNewUsersWithRepository and
-          Repository.where(name: repo_name).first.nil?
-        repo = Repository.create!(name: repo_name, owner_uuid: uuid)
-        Link.create!(tail_uuid: uuid, head_uuid: repo.uuid,
-                     link_class: "permission", name: "can_manage")
-      end
-    end
   end
 
   # Send notification if the user saved profile for the first time
index f413188b54b2ba54236a8ea5ce2ab51c002cb434..76d8cb9736d524c942da63bad3894c1f87e1a8a4 100644 (file)
@@ -147,6 +147,39 @@ class Arvados::V1::GroupsControllerTest < ActionController::TestCase
     refute_includes found_uuids, specimens(:in_asubproject).uuid, "specimen appeared unexpectedly in home project"
   end
 
+  test "list collections in home project" do
+    authorize_with :active
+    get(:contents, params: {
+          format: :json,
+          filters: [
+            ['uuid', 'is_a', 'arvados#collection'],
+          ],
+          limit: 200,
+          id: users(:active).uuid,
+        })
+    assert_response :success
+    found_uuids = json_response['items'].collect { |i| i['uuid'] }
+    assert_includes found_uuids, collections(:collection_owned_by_active).uuid, "collection did not appear in home project"
+    refute_includes found_uuids, collections(:collection_owned_by_active_past_version_1).uuid, "collection appeared unexpectedly in home project"
+  end
+
+  test "list collections in home project, including old versions" do
+    authorize_with :active
+    get(:contents, params: {
+          format: :json,
+          include_old_versions: true,
+          filters: [
+            ['uuid', 'is_a', 'arvados#collection'],
+          ],
+          limit: 200,
+          id: users(:active).uuid,
+        })
+    assert_response :success
+    found_uuids = json_response['items'].collect { |i| i['uuid'] }
+    assert_includes found_uuids, collections(:collection_owned_by_active).uuid, "collection did not appear in home project"
+    assert_includes found_uuids, collections(:collection_owned_by_active_past_version_1).uuid, "old collection version did not appear in home project"
+  end
+
   test "user with project read permission can see project collections" do
     authorize_with :project_viewer
     get :contents, params: {
index 764f3a8d1dd395a7bb84b365144ca114bf3c92ff..89feecb454a9fa74541b7328cf282287ee46da6e 100644 (file)
@@ -84,7 +84,7 @@ class Arvados::V1::SchemaControllerTest < ActionController::TestCase
     group_index_params = discovery_doc['resources']['groups']['methods']['index']['parameters']
     group_contents_params = discovery_doc['resources']['groups']['methods']['contents']['parameters']
 
-    assert_equal group_contents_params.keys.sort, (group_index_params.keys - ['select'] + ['uuid', 'recursive', 'include']).sort
+    assert_equal group_contents_params.keys.sort, (group_index_params.keys - ['select'] + ['uuid', 'recursive', 'include', 'include_old_versions']).sort
 
     recursive_param = group_contents_params['recursive']
     assert_equal 'boolean', recursive_param['type']
index 9ad93c2ee6af4f3cb6a6a42f10e85468b90e8946..4323884529005102eefbe4173aff610847779380 100644 (file)
@@ -325,30 +325,43 @@ class RemoteUsersTest < ActionDispatch::IntegrationTest
     assert_equal 'barney', json_response['username']
   end
 
-  test 'get inactive user from Login cluster when AutoSetupNewUsers is set' do
-    Rails.configuration.Login.LoginCluster = 'zbbbb'
-    Rails.configuration.Users.AutoSetupNewUsers = true
-    @stub_content = {
-      uuid: 'zbbbb-tpzed-000000000000001',
-      email: 'foo@example.com',
-      username: 'barney',
-      is_admin: false,
-      is_active: false,
-      is_invited: false,
-    }
-    get '/arvados/v1/users/current',
-      params: {format: 'json'},
-      headers: auth(remote: 'zbbbb')
-    assert_response :success
-    assert_equal 'zbbbb-tpzed-000000000000001', json_response['uuid']
-    assert_equal false, json_response['is_admin']
-    assert_equal false, json_response['is_active']
-    assert_equal false, json_response['is_invited']
-    assert_equal 'foo@example.com', json_response['email']
-    assert_equal 'barney', json_response['username']
+  [true, false].each do |trusted|
+    [true, false].each do |logincluster|
+      [true, false].each do |admin|
+        [true, false].each do |active|
+          [true, false].each do |autosetup|
+            [true, false].each do |invited|
+              test "get invited=#{invited}, active=#{active}, admin=#{admin} user from #{if logincluster then "Login" else "peer" end} cluster when AutoSetupNewUsers=#{autosetup} ActivateUsers=#{trusted}" do
+                Rails.configuration.Login.LoginCluster = 'zbbbb' if logincluster
+                Rails.configuration.RemoteClusters['zbbbb'].ActivateUsers = trusted
+                Rails.configuration.Users.AutoSetupNewUsers = autosetup
+                @stub_content = {
+                  uuid: 'zbbbb-tpzed-000000000000001',
+                  email: 'foo@example.com',
+                  username: 'barney',
+                  is_admin: admin,
+                  is_active: active,
+                  is_invited: invited,
+                }
+                get '/arvados/v1/users/current',
+                    params: {format: 'json'},
+                    headers: auth(remote: 'zbbbb')
+                assert_response :success
+                assert_equal 'zbbbb-tpzed-000000000000001', json_response['uuid']
+                assert_equal (logincluster && admin && invited && active), json_response['is_admin']
+                assert_equal (invited and (logincluster || trusted || autosetup)), json_response['is_invited']
+                assert_equal (invited and (logincluster || trusted) and active), json_response['is_active']
+                assert_equal 'foo@example.com', json_response['email']
+                assert_equal 'barney', json_response['username']
+              end
+            end
+          end
+        end
+      end
+    end
   end
 
-    test 'get active user from Login cluster when AutoSetupNewUsers is set' do
+  test 'get active user from Login cluster when AutoSetupNewUsers is set' do
     Rails.configuration.Login.LoginCluster = 'zbbbb'
     Rails.configuration.Users.AutoSetupNewUsers = true
     @stub_content = {
index b6d66230db6e2bbb0a06bbe0add823fdb397642d..f973c6ba1fa39337125716b76c6bd7cb928b2a18 100644 (file)
@@ -387,7 +387,7 @@ class UserTest < ActiveSupport::TestCase
     [false, active_notify_list, inactive_notify_list, "&4a_d9.@example.com", true, true, "ad9"],
     [false, active_notify_list, inactive_notify_list, "&4a_d9.@example.com", false, false, "ad9"],
   ].each do |active, new_user_recipients, inactive_recipients, email, auto_setup_vm, auto_setup_repo, expect_username|
-    test "create new user with auto setup #{active} #{email} #{auto_setup_vm} #{auto_setup_repo}" do
+    test "create new user with auto setup active=#{active} email=#{email} vm=#{auto_setup_vm} repo=#{auto_setup_repo}" do
       set_user_from_auth :admin
 
       Rails.configuration.Users.AutoSetupNewUsers = true
@@ -621,6 +621,7 @@ class UserTest < ActiveSupport::TestCase
                           Rails.configuration.Users.AutoSetupNewUsersWithRepository),
                          named_repo.uuid, user.uuid, "permission", "can_manage")
     end
+
     # Check for VM login.
     if (auto_vm_uuid = Rails.configuration.Users.AutoSetupNewUsersWithVmUUID) != ""
       verify_link_exists(can_setup, auto_vm_uuid, user.uuid,
@@ -677,7 +678,7 @@ class UserTest < ActiveSupport::TestCase
                            tail_uuid: tail_uuid,
                            link_class: link_class,
                            name: link_name)
-    assert_equal link_exists, all_links.any?, "Link #{'not' if link_exists} found for #{link_name} #{link_class} #{property_value}"
+    assert_equal link_exists, all_links.any?, "Link#{' not' if link_exists} found for #{link_name} #{link_class} #{property_value}"
     if link_exists && property_name && property_value
       all_links.each do |link|
         assert_equal true, all_links.first.properties[property_name].start_with?(property_value), 'Property not found in link'
index 9aabff42929838a1f9748362a63eeed003775a64..36804bf5b36601e6e12b9bf1c86a979abc6dd40e 100644 (file)
@@ -15,12 +15,13 @@ def git_version_at_commit():
     return myversion
 
 def save_version(setup_dir, module, v):
-  with open(os.path.join(setup_dir, module, "_version.py"), 'wt') as fp:
-      return fp.write("__version__ = '%s'\n" % v)
+    v = v.replace("~dev", ".dev").replace("~rc", "rc")
+    with open(os.path.join(setup_dir, module, "_version.py"), 'wt') as fp:
+        return fp.write("__version__ = '%s'\n" % v)
 
 def read_version(setup_dir, module):
-  with open(os.path.join(setup_dir, module, "_version.py"), 'rt') as fp:
-      return re.match("__version__ = '(.*)'$", fp.read()).groups()[0]
+    with open(os.path.join(setup_dir, module, "_version.py"), 'rt') as fp:
+        return re.match("__version__ = '(.*)'$", fp.read()).groups()[0]
 
 def get_version(setup_dir, module):
     env_version = os.environ.get("ARVADOS_BUILDING_VERSION")
index 0c653694f566b3883ccd2682b05d446eff849bd0..0d307c1be88f16aa88089e803013ba01f5a4fa04 100644 (file)
@@ -30,12 +30,13 @@ def git_version_at_commit():
     return myversion
 
 def save_version(setup_dir, module, v):
-  with open(os.path.join(setup_dir, module, "_version.py"), 'wt') as fp:
-      return fp.write("__version__ = '%s'\n" % v)
+    v = v.replace("~dev", ".dev").replace("~rc", "rc")
+    with open(os.path.join(setup_dir, module, "_version.py"), 'wt') as fp:
+        return fp.write("__version__ = '%s'\n" % v)
 
 def read_version(setup_dir, module):
-  with open(os.path.join(setup_dir, module, "_version.py"), 'rt') as fp:
-      return re.match("__version__ = '(.*)'$", fp.read()).groups()[0]
+    with open(os.path.join(setup_dir, module, "_version.py"), 'rt') as fp:
+        return re.match("__version__ = '(.*)'$", fp.read()).groups()[0]
 
 def get_version(setup_dir, module):
     env_version = os.environ.get("ARVADOS_BUILDING_VERSION")
index 8682eac2dd08b5aaa8f308330ca4a2eba06cf34e..be81bb68c71c74a439a2dbd02cf11acefea127b7 100644 (file)
 //
 // Download URLs
 //
-// The following "same origin" URL patterns are supported for public
-// collections and collections shared anonymously via secret links
-// (i.e., collections which can be served by keep-web without making
-// use of any implicit credentials like cookies). See "Same-origin
-// URLs" below.
-//
-//   http://collections.example.com/c=uuid_or_pdh/path/file.txt
-//   http://collections.example.com/c=uuid_or_pdh/t=TOKEN/path/file.txt
-//
-// The following "multiple origin" URL patterns are supported for all
-// collections:
-//
-//   http://uuid_or_pdh--collections.example.com/path/file.txt
-//   http://uuid_or_pdh--collections.example.com/t=TOKEN/path/file.txt
-//
-// In the "multiple origin" form, the string "--" can be replaced with
-// "." with identical results (assuming the downstream proxy is
-// configured accordingly). These two are equivalent:
-//
-//   http://uuid_or_pdh--collections.example.com/path/file.txt
-//   http://uuid_or_pdh.collections.example.com/path/file.txt
-//
-// The first form (with "--" instead of ".") avoids the cost and
-// effort of deploying a wildcard TLS certificate for
-// *.collections.example.com at sites that already have a wildcard
-// certificate for *.example.com. The second form is likely to be
-// easier to configure, and more efficient to run, on a downstream
-// proxy.
-//
-// In all of the above forms, the "collections.example.com" part can
-// be anything at all: keep-web itself ignores everything after the
-// first "." or "--". (Of course, in order for clients to connect at
-// all, DNS and any relevant proxies must be configured accordingly.)
-//
-// In all of the above forms, the "uuid_or_pdh" part can be either a
-// collection UUID or a portable data hash with the "+" character
-// optionally replaced by "-". (When "uuid_or_pdh" appears in the
-// domain name, replacing "+" with "-" is mandatory, because "+" is
-// not a valid character in a domain name.)
-//
-// In all of the above forms, a top level directory called "_" is
-// skipped. In cases where the "path/file.txt" part might start with
-// "t=" or "c=" or "_/", links should be constructed with a leading
-// "_/" to ensure the top level directory is not interpreted as a
-// token or collection ID.
-//
-// Assuming there is a collection with UUID
-// zzzzz-4zz18-znfnqtbbv4spc3w and portable data hash
-// 1f4b0bc7583c2a7f9102c395f4ffc5e3+45, the following URLs are
-// interchangeable:
-//
-//   http://zzzzz-4zz18-znfnqtbbv4spc3w.collections.example.com/foo/bar.txt
-//   http://zzzzz-4zz18-znfnqtbbv4spc3w.collections.example.com/_/foo/bar.txt
-//   http://zzzzz-4zz18-znfnqtbbv4spc3w--collections.example.com/_/foo/bar.txt
-//
-// The following URLs are read-only, but otherwise interchangeable
-// with the above:
-//
-//   http://1f4b0bc7583c2a7f9102c395f4ffc5e3-45--foo.example.com/foo/bar.txt
-//   http://1f4b0bc7583c2a7f9102c395f4ffc5e3-45--.invalid/foo/bar.txt
-//   http://collections.example.com/by_id/1f4b0bc7583c2a7f9102c395f4ffc5e3%2B45/foo/bar.txt
-//   http://collections.example.com/by_id/zzzzz-4zz18-znfnqtbbv4spc3w/foo/bar.txt
-//
-// If the collection is named "MyCollection" and located in a project
-// called "MyProject" which is in the home project of a user with
-// username is "bob", the following read-only URL is also available
-// when authenticating as bob:
-//
-//   http://collections.example.com/users/bob/MyProject/MyCollection/foo/bar.txt
-//
-// An additional form is supported specifically to make it more
-// convenient to maintain support for existing Workbench download
-// links:
-//
-//   http://collections.example.com/collections/download/uuid_or_pdh/TOKEN/foo/bar.txt
-//
-// A regular Workbench "download" link is also accepted, but
-// credentials passed via cookie, header, etc. are ignored. Only
-// public data can be served this way:
-//
-//   http://collections.example.com/collections/uuid_or_pdh/foo/bar.txt
-//
-// Collections can also be accessed (read-only) via "/by_id/X" where X
-// is a UUID or portable data hash.
-//
-// Authorization mechanisms
-//
-// A token can be provided in an Authorization header:
-//
-//   Authorization: OAuth2 o07j4px7RlJK4CuMYp7C0LDT4CzR1J1qBE5Avo7eCcUjOTikxK
-//
-// A base64-encoded token can be provided in a cookie named "api_token":
-//
-//   Cookie: api_token=bzA3ajRweDdSbEpLNEN1TVlwN0MwTERUNEN6UjFKMXFCRTVBdm83ZUNjVWpPVGlreEs=
-//
-// A token can be provided in an URL-encoded query string:
-//
-//   GET /foo/bar.txt?api_token=o07j4px7RlJK4CuMYp7C0LDT4CzR1J1qBE5Avo7eCcUjOTikxK
-//
-// A suitably encoded token can be provided in a POST body if the
-// request has a content type of application/x-www-form-urlencoded or
-// multipart/form-data:
-//
-//   POST /foo/bar.txt
-//   Content-Type: application/x-www-form-urlencoded
-//   [...]
-//   api_token=o07j4px7RlJK4CuMYp7C0LDT4CzR1J1qBE5Avo7eCcUjOTikxK
-//
-// If a token is provided in a query string or in a POST request, the
-// response is an HTTP 303 redirect to an equivalent GET request, with
-// the token stripped from the query string and added to a cookie
-// instead.
-//
-// Indexes
-//
-// Keep-web returns a generic HTML index listing when a directory is
-// requested with the GET method. It does not serve a default file
-// like "index.html". Directory listings are also returned for WebDAV
-// PROPFIND requests.
-//
-// Compatibility
-//
-// Client-provided authorization tokens are ignored if the client does
-// not provide a Host header.
-//
-// In order to use the query string or a POST form authorization
-// mechanisms, the client must follow 303 redirects; the client must
-// accept cookies with a 303 response and send those cookies when
-// performing the redirect; and either the client or an intervening
-// proxy must resolve a relative URL ("//host/path") if given in a
-// response Location header.
-//
-// Intranet mode
-//
-// Normally, Keep-web accepts requests for multiple collections using
-// the same host name, provided the client's credentials are not being
-// used. This provides insufficient XSS protection in an installation
-// where the "anonymously accessible" data is not truly public, but
-// merely protected by network topology.
-//
-// In such cases -- for example, a site which is not reachable from
-// the internet, where some data is world-readable from Arvados's
-// perspective but is intended to be available only to users within
-// the local network -- the downstream proxy should configured to
-// return 401 for all paths beginning with "/c=".
-//
-// Same-origin URLs
-//
-// Without the same-origin protection outlined above, a web page
-// stored in collection X could execute JavaScript code that uses the
-// current viewer's credentials to download additional data from
-// collection Y -- data which is accessible to the current viewer, but
-// not to the author of collection X -- from the same origin
-// (``https://collections.example.com/'') and upload it to some other
-// site chosen by the author of collection X.
+// See http://doc.arvados.org/api/keep-web-urls.html
 //
 // Attachment-Only host
 //
index b45f8692be5b83254318c3b9edc3854ebade9913..8e338d2f2b697f47b45f1d3003d7c5c6f4607873 100644 (file)
@@ -18,6 +18,7 @@ begin
   else
     version = `#{__dir__}/../../build/version-at-commit.sh #{git_hash}`.encode('utf-8').strip
   end
+  version = version.sub("~dev", ".dev").sub("~rc", ".rc")
   git_timestamp = Time.at(git_timestamp.to_i).utc
 ensure
   ENV["GIT_DIR"] = git_dir
index 6a86cbe7a8307e1683dbd09ea506bc8cd79f52e3..a67df1511723fecf169f004a42abf1cabceec511 100644 (file)
@@ -2,7 +2,7 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-// Arvados-ws exposes Arvados APIs (currently just one, the
+// Package ws exposes Arvados APIs (currently just one, the
 // cache-invalidation event feed at "ws://.../websocket") to
 // websocket clients.
 //
index cd72ec06bbfa9ecf0bf4dbc550886e246556a0df..a180b43630471f0c92ded944e6e1b8290ece4245 100755 (executable)
@@ -622,10 +622,9 @@ sv stop keepproxy
 cd /usr/src/arvados/services/api
 export DISABLE_DATABASE_ENVIRONMENT_CHECK=1
 export RAILS_ENV=development
-bundle exec rake db:drop
+flock $GEM_HOME/gems.lock bundle exec rake db:drop
 rm $ARVADOS_CONTAINER_PATH/api_database_setup
 rm $ARVADOS_CONTAINER_PATH/superuser_token
-rm $ARVADOS_CONTAINER_PATH/keepproxy-uuid
 sv start api
 sv start controller
 sv start websockets
index 948eb00a559acfbb8f7ffaf48f8a79cbc9c8bfab..708af17d5cbc13b5fbea74620f34a05c54214247 100755 (executable)
@@ -105,7 +105,7 @@ Clusters:
       WebDAVDownload:
         InternalURLs:
           "http://localhost:${services[keep-web]}/": {}
-        ExternalURL: "https://$localip:${services[keep-web-ssl]}/"
+        ExternalURL: "https://$localip:${services[keep-web-dl-ssl]}/"
       Composer:
         ExternalURL: "https://$localip:${services[composer]}"
       Controller:
index 48d3566405f982e210bc2af673a6f6df6fea962b..eb53e190490aa963bddf706be1bc7893053f61e4 100644 (file)
@@ -38,6 +38,7 @@ services=(
   [arv-git-httpd]=9001
   [keep-web]=9003
   [keep-web-ssl]=9002
+  [keep-web-dl-ssl]=9004
   [keepproxy]=25100
   [keepproxy-ssl]=25101
   [keepstore0]=25107
index 05d11dfde8121eb364dabc193185e4ba7fe4162c..55edce3f9d8b032024d3c737f32c891fcd36224b 100755 (executable)
@@ -15,7 +15,7 @@ export HOME=$ARVADOS_CONTAINER_PATH
 chown arvbox /dev/stderr
 
 # Load our custom sysctl.conf entries
-/sbin/sysctl -p
+/sbin/sysctl -p >/dev/null
 
 if test -z "$1" ; then
     exec chpst -u arvbox:arvbox:docker $0-service
index b586afb65c70496ddd914cab5645e935d9230c86..d2691e7ed6bd2acdf96c1be2634c72fb3da30c03 100755 (executable)
@@ -31,4 +31,6 @@ if test "$1" = "--only-setup" ; then
     exit
 fi
 
+touch $ARVADOS_CONTAINER_PATH/api.ready
+
 exec bundle exec passenger start --port=${services[api]}
index fa7aed1ef5c91c4855a0710f7206b0555a0081cd..36566c9d9b5ac1b95dd3c85c40f148ed123125b0 100755 (executable)
@@ -8,6 +8,11 @@ set -ex -o pipefail
 
 . /usr/local/lib/arvbox/common.sh
 
+if test "$1" != "--only-deps" ; then
+  while [ ! -f $ARVADOS_CONTAINER_PATH/api.ready ]; do
+    sleep 1
+  done
+fi
 
 cd /usr/src/arvados/doc
 run_bundler --without=development
index 967c75f9064c21a1134c582ae1e5df6793eb42f9..c60c15bfc53887e45cfeb84dd8f119aedcf544ee 100755 (executable)
@@ -8,6 +8,12 @@ set -eux -o pipefail
 
 . /usr/local/lib/arvbox/common.sh
 
+if test "$1" != "--only-deps" ; then
+  while [ ! -f $ARVADOS_CONTAINER_PATH/api.ready ]; do
+    sleep 1
+  done
+fi
+
 mkdir -p $ARVADOS_CONTAINER_PATH/git
 
 export ARVADOS_API_HOST=$localip:${services[controller-ssl]}
index cf5ccd724b885ee84233b03e0259ef9c5906e8cc..0374c43e9c5360ab2d9f2a3720560b4af49536de 100755 (executable)
@@ -17,27 +17,4 @@ if test "$1" = "--only-deps" ; then
     exit
 fi
 
-export ARVADOS_API_HOST=$localip:${services[controller-ssl]}
-export ARVADOS_API_HOST_INSECURE=1
-export ARVADOS_API_TOKEN=$(cat $ARVADOS_CONTAINER_PATH/superuser_token)
-
-set +e
-read -rd $'\000' keepservice <<EOF
-{
- "service_host":"$localip",
- "service_port":${services[keepproxy-ssl]},
- "service_ssl_flag":true,
- "service_type":"proxy"
-}
-EOF
-set -e
-
-if test -s $ARVADOS_CONTAINER_PATH/keepproxy-uuid ; then
-    keep_uuid=$(cat $ARVADOS_CONTAINER_PATH/keepproxy-uuid)
-    arv keep_service update --uuid $keep_uuid --keep-service "$keepservice"
-else
-    UUID=$(arv --format=uuid keep_service create --keep-service "$keepservice")
-    echo $UUID > $ARVADOS_CONTAINER_PATH/keepproxy-uuid
-fi
-
 exec /usr/local/bin/keepproxy
index 82db92137053f83f0282f24211e63824a7bb4a0c..991927be70645fc06ee3544663272db2fe2b8c23 100755 (executable)
@@ -144,6 +144,20 @@ http {
       proxy_redirect off;
     }
   }
+  server {
+    listen *:${services[keep-web-dl-ssl]} ssl default_server;
+    server_name keep-web-dl;
+    ssl_certificate "${server_cert}";
+    ssl_certificate_key "${server_cert_key}";
+    client_max_body_size 0;
+    location  / {
+      proxy_pass http://keep-web;
+      proxy_set_header Host \$http_host;
+      proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
+      proxy_set_header X-Forwarded-Proto https;
+      proxy_redirect off;
+    }
+  }
 
   upstream keepproxy {
     server localhost:${services[keepproxy]};
index 5369af31d0c086cd986e2b9de877b5e196439d28..0c7213f8696143f6870ce1c3709e0d791e518d77 100755 (executable)
@@ -9,6 +9,12 @@ set -ex -o pipefail
 
 . /usr/local/lib/arvbox/common.sh
 
+if test "$1" != "--only-deps" ; then
+  while [ ! -f $ARVADOS_CONTAINER_PATH/api.ready ]; do
+    sleep 1
+  done
+fi
+
 cd /usr/src/arvados/services/login-sync
 run_bundler --binstubs=$PWD/binstubs
 ln -sf /usr/src/arvados/services/login-sync/binstubs/arvados-login-sync /usr/local/bin/arvados-login-sync
index b40d4f5699abd727b7fd54a0cbcdb30bcaba8617..32efea51b1f13fa7c66a49ca7b3009952e43f778 100755 (executable)
@@ -8,6 +8,12 @@ set -ex -o pipefail
 
 .  /usr/local/lib/arvbox/common.sh
 
+if test "$1" != "--only-deps" ; then
+  while [ ! -f $ARVADOS_CONTAINER_PATH/api.ready ]; do
+    sleep 1
+  done
+fi
+
 cd /usr/src/arvados/apps/workbench
 
 if test -s $ARVADOS_CONTAINER_PATH/workbench_rails_env ; then
@@ -37,25 +43,5 @@ set -u
 
 secret_token=$(cat $ARVADOS_CONTAINER_PATH/workbench_secret_token)
 
-if test -a /usr/src/arvados/apps/workbench/config/arvados_config.rb ; then
-    rm -f config/application.yml
-else
-cat >config/application.yml <<EOF
-$RAILS_ENV:
-  secret_token: $secret_token
-  arvados_login_base: https://$localip:${services[controller-ssl]}/login
-  arvados_v1_base: https://$localip:${services[controller-ssl]}/arvados/v1
-  arvados_insecure_https: false
-  keep_web_download_url: https://$localip:${services[keep-web-ssl]}/c=%{uuid_or_pdh}
-  keep_web_url: https://$localip:${services[keep-web-ssl]}/c=%{uuid_or_pdh}
-  arvados_docsite: http://$localip:${services[doc]}/
-  force_ssl: false
-  composer_url: http://$localip:${services[composer]}
-  workbench2_url: https://$localip:${services[workbench2-ssl]}
-EOF
-
-(cd config && /usr/local/lib/arvbox/yml_override.py application.yml)
-fi
-
 RAILS_GROUPS=assets flock $GEM_HOME/gems.lock bundle exec rake npm:install
 flock $GEM_HOME/gems.lock bundle exec rake assets:precompile
index 8c3c49efd65dced676317333f0fa755280d9bde2..f956eecc61b6118885fb78a8ae1cf1cadfdda0c6 100755 (executable)
@@ -8,6 +8,12 @@ set -ex -o pipefail
 
 .  /usr/local/lib/arvbox/common.sh
 
+if test "$1" != "--only-deps" ; then
+  while [ ! -f $ARVADOS_CONTAINER_PATH/api.ready ]; do
+    sleep 1
+  done
+fi
+
 cd /usr/src/workbench2
 
 npm -d install --prefix /usr/local --global yarn@1.17.3
index 0c653694f566b3883ccd2682b05d446eff849bd0..0d307c1be88f16aa88089e803013ba01f5a4fa04 100644 (file)
@@ -30,12 +30,13 @@ def git_version_at_commit():
     return myversion
 
 def save_version(setup_dir, module, v):
-  with open(os.path.join(setup_dir, module, "_version.py"), 'wt') as fp:
-      return fp.write("__version__ = '%s'\n" % v)
+    v = v.replace("~dev", ".dev").replace("~rc", "rc")
+    with open(os.path.join(setup_dir, module, "_version.py"), 'wt') as fp:
+        return fp.write("__version__ = '%s'\n" % v)
 
 def read_version(setup_dir, module):
-  with open(os.path.join(setup_dir, module, "_version.py"), 'rt') as fp:
-      return re.match("__version__ = '(.*)'$", fp.read()).groups()[0]
+    with open(os.path.join(setup_dir, module, "_version.py"), 'rt') as fp:
+        return re.match("__version__ = '(.*)'$", fp.read()).groups()[0]
 
 def get_version(setup_dir, module):
     env_version = os.environ.get("ARVADOS_BUILDING_VERSION")
index 4d03ba89e327aa7db1bd9f08808e15d3f0487c9f..24e838c8f1ec64434a13652b36b18689ddb5a216 100644 (file)
@@ -275,7 +275,13 @@ func GetConfig() (config ConfigParams, err error) {
        if !u.IsActive || !u.IsAdmin {
                return config, fmt.Errorf("current user (%s) is not an active admin user", u.UUID)
        }
-       config.SysUserUUID = u.UUID[:12] + "000000000000000"
+
+       var ac struct{ ClusterID string }
+       err = config.Client.RequestAndDecode(&ac, "GET", "arvados/v1/config", nil, nil)
+       if err != nil {
+               return config, fmt.Errorf("error getting the exported config: %s", err)
+       }
+       config.SysUserUUID = ac.ClusterID + "-tpzed-000000000000000"
 
        // Set up remote groups' parent
        if err = SetParentGroup(&config); err != nil {
@@ -432,7 +438,7 @@ func ProcessFile(
                                "group_class": "role",
                        }
                        if e := CreateGroup(cfg, &newGroup, groupData); e != nil {
-                               err = fmt.Errorf("error creating group named %q: %s", groupName, err)
+                               err = fmt.Errorf("error creating group named %q: %s", groupName, e)
                                return
                        }
                        // Update cached group data
index 2da8c1cdde4bb2cf131e9afcd520eec7f4e5ed47..ec2f18a307d70c9767efcdef96574aa18d2cc862 100644 (file)
@@ -26,14 +26,6 @@ type TestSuite struct {
        users map[string]arvados.User
 }
 
-func (s *TestSuite) SetUpSuite(c *C) {
-       arvadostest.StartAPI()
-}
-
-func (s *TestSuite) TearDownSuite(c *C) {
-       arvadostest.StopAPI()
-}
-
 func (s *TestSuite) SetUpTest(c *C) {
        ac := arvados.NewClientFromEnv()
        u, err := ac.CurrentUser()