Merge branch '16312-s3-signature-v4'
authorTom Clegg <tom@tomclegg.ca>
Wed, 20 May 2020 14:14:41 +0000 (10:14 -0400)
committerTom Clegg <tom@tomclegg.ca>
Wed, 20 May 2020 14:14:41 +0000 (10:14 -0400)
closes #16312

Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tom@tomclegg.ca>

48 files changed:
apps/workbench/test/integration/anonymous_access_test.rb
apps/workbench/test/integration/collections_test.rb
build/package-testing/test-package-python3-arvados-cwl-runner.sh [new file with mode: 0755]
build/rails-package-scripts/arvados-sso-server.sh
build/run-library.sh
build/run-tests.sh
doc/_config.yml
doc/admin/metrics.html.textile.liquid
doc/admin/migrating-providers.html.textile.liquid
doc/admin/upgrading.html.textile.liquid
doc/admin/user-management.html.textile.liquid
doc/install/google-auth.html.textile.liquid [deleted file]
doc/install/install-components.html.textile.liquid [deleted file]
doc/install/install-manual-prerequisites.html.textile.liquid
doc/install/install-sso.html.textile.liquid [deleted file]
doc/install/install-ws.html.textile.liquid
doc/install/setup-login.html.textile.liquid
go.mod
go.sum
lib/config/config.default.yml
lib/config/deprecated.go
lib/config/deprecated_test.go
lib/config/export.go
lib/config/generated_config.go
lib/controller/federation/login_test.go
lib/controller/handler_test.go
lib/controller/integration_test.go
lib/controller/localdb/login.go
lib/controller/localdb/login_google.go
lib/controller/localdb/login_google_test.go
lib/controller/localdb/login_ldap.go [new file with mode: 0644]
lib/controller/localdb/login_ldap_docker_test.go [new file with mode: 0644]
lib/controller/localdb/login_ldap_docker_test.sh [moved from lib/controller/localdb/login_pam_docker_test.sh with 63% similarity]
lib/controller/localdb/login_ldap_test.go [new file with mode: 0644]
lib/controller/localdb/login_pam.go
lib/controller/localdb/login_pam_docker_test.go [deleted file]
lib/controller/localdb/login_pam_test.go
lib/install/deps.go
sdk/cwl/fpm-info.sh
sdk/go/arvados/config.go
sdk/python/tests/run_test_server.py
services/api/app/controllers/arvados/v1/users_controller.rb
services/api/app/models/user.rb
services/api/config/arvados_config.rb
services/api/config/initializers/omniauth_init.rb
services/keepstore/unix_volume.go
services/keepstore/unix_volume_test.go
tools/arvbox/lib/arvbox/docker/cluster-config.sh

index 0842635f603ff00ad93dbb15581950f860c37567..cbbe28a6f3d1eb2f61ca6a8d11e815aa0702fb3f 100644 (file)
@@ -117,6 +117,7 @@ class AnonymousAccessTest < ActionDispatch::IntegrationTest
   end
 
   test 'view file' do
+    need_selenium "phantomjs does not follow redirects reliably, maybe https://github.com/ariya/phantomjs/issues/10389"
     magic = rand(2**512).to_s 36
     owner = api_fixture('groups')['anonymously_accessible_project']['uuid']
     col = upload_data_and_get_collection(magic, 'admin', "Hello\\040world.txt", owner)
index 87d3d678d174c99e03f527c58a6970f05c122f11..e7b27fff86377c4013a216bc897bf1cc7016b7f4 100644 (file)
@@ -53,6 +53,8 @@ class CollectionsTest < ActionDispatch::IntegrationTest
   end
 
   test "can download an entire collection with a reader token" do
+    need_selenium "phantomjs does not follow redirects reliably, maybe https://github.com/ariya/phantomjs/issues/10389"
+
     token = api_token('active')
     data = "foo\nfile\n"
     datablock = `echo -n #{data.shellescape} | ARVADOS_API_TOKEN=#{token.shellescape} arv-put --no-progress --raw -`.strip
@@ -68,24 +70,16 @@ class CollectionsTest < ActionDispatch::IntegrationTest
     token = api_fixture('api_client_authorizations')['active_all_collections']['api_token']
     url_head = "/collections/download/#{uuid}/#{token}/"
     visit url_head
+    assert_text "You can download individual files listed below"
     # It seems that Capybara can't inspect tags outside the body, so this is
     # a very blunt approach.
     assert_no_match(/<\s*meta[^>]+\bnofollow\b/i, page.html,
                     "wget prohibited from recursing the collection page")
     # Look at all the links that wget would recurse through using our
     # recommended options, and check that it's exactly the file list.
-    hrefs = page.all('a').map do |anchor|
-      link = anchor[:href] || ''
-      if link.start_with? url_head
-        link[url_head.size .. -1]
-      elsif link.start_with? '/'
-        nil
-      else
-        link
-      end
-    end
-    assert_equal(['./foo'], hrefs.compact.sort,
-                 "download page did provide strictly file links")
+    hrefs = []
+    page.html.scan(/href="(.*?)"/) { |m| hrefs << m[0] }
+    assert_equal(['./foo'], hrefs, "download page did provide strictly file links")
     click_link "foo"
     assert_text "foo\nfile\n"
   end
diff --git a/build/package-testing/test-package-python3-arvados-cwl-runner.sh b/build/package-testing/test-package-python3-arvados-cwl-runner.sh
new file mode 100755 (executable)
index 0000000..99327c0
--- /dev/null
@@ -0,0 +1,8 @@
+#!/bin/sh
+# Copyright (C) The Arvados Authors. All rights reserved.
+#
+# SPDX-License-Identifier: AGPL-3.0
+
+set -e
+
+arvados-cwl-runner --version
index fff582bb18dbb591446713356ab12f8335d8f66a..e88da0d3a6733f71e3edb33922ed6a9b9a837936 100644 (file)
@@ -8,6 +8,6 @@
 PACKAGE_NAME=arvados-sso-server
 INSTALL_PATH=/var/www/arvados-sso
 CONFIG_PATH=/etc/arvados/sso
-DOC_URL="http://doc.arvados.org/install/install-sso.html#configure"
+DOC_URL="https://doc.arvados.org/v2.0/install/install-sso.html#configure"
 RAILSPKG_DATABASE_LOAD_TASK=db:schema:load
 RAILSPKG_SUPPORTS_CONFIG_CHECK=0
index beb23f563a637abe99186a92f6698eed3e993b4d..7dc293ab61e2612cb7a429a06f54a48d2a48b614 100755 (executable)
@@ -465,7 +465,7 @@ fpm_build_virtualenv () {
   fi
 
   # arvados-python-client sdist should always be built, to be available
-  # for other dependant packages.
+  # for other dependent packages.
   if [[ -n "$ONLY_BUILD" ]] && [[ "arvados-python-client" != "$PKG" ]] && [[ "$PYTHON_PKG" != "$ONLY_BUILD" ]] && [[ "$PKG" != "$ONLY_BUILD" ]]; then
     return 0
   fi
@@ -613,7 +613,7 @@ fpm_build_virtualenv () {
   # 12271 - As FPM-generated packages don't include scripts by default, the
   # packages cleanup on upgrade depends on files being listed on the %files
   # section in the generated SPEC files. To remove DIRECTORIES, they need to
-  # be listed in that sectiontoo, so we need to add this parameter to properly
+  # be listed in that section too, so we need to add this parameter to properly
   # remove lingering dirs. But this only works for python2: if used on
   # python33, it includes dirs like /opt/rh/python33 that belong to
   # other packages.
@@ -673,9 +673,9 @@ fpm_build_virtualenv () {
   done
 
   # make sure the systemd service file ends up in the right place
-  # currently only used by arvados-docker-cleaner
+  # used by arvados-docker-cleaner and arvados-node-manager
   if [[ -e "${systemd_unit}" ]]; then
-    COMMAND_ARR+=("usr/share/python3/dist/$PKG/share/doc/$PKG/$PKG.service=/lib/systemd/system/$PKG.service")
+    COMMAND_ARR+=("usr/share/$python/dist/$PKG/share/doc/$PKG/$PKG.service=/lib/systemd/system/$PKG.service")
   fi
 
   COMMAND_ARR+=("${fpm_args[@]}")
index 0212d1bc0e13e7b6202a04f4da00436a6c278ed1..ff6ead0facc26bbb0e1141d118b4cd81a70ec4c0 100755 (executable)
@@ -1205,6 +1205,7 @@ help_interactive() {
     echo "== Interactive commands:"
     echo "TARGET                 (short for 'test DIR')"
     echo "test TARGET"
+    echo "10 test TARGET         (run test 10 times)"
     echo "test TARGET:py3        (test with python3)"
     echo "test TARGET -check.vv  (pass arguments to test)"
     echo "install TARGET"
@@ -1265,6 +1266,10 @@ else
     while read -p 'What next? ' -e -i "$nextcmd" nextcmd; do
         history -s "$nextcmd"
         history -w
+        count=1
+        if [[ "${nextcmd}" =~ ^[0-9] ]]; then
+          read count nextcmd <<<"${nextcmd}"
+        fi
         read verb target opts <<<"${nextcmd}"
         target="${target%/}"
         target="${target/\/:/:}"
@@ -1284,11 +1289,14 @@ else
                         ${verb}_${target}
                         ;;
                     *)
-                       argstarget=${target%:py3}
+                        argstarget=${target%:py3}
                         testargs["$argstarget"]="${opts}"
                         tt="${testfuncargs[${target}]}"
                         tt="${tt:-$target}"
-                        do_$verb $tt
+                        while [ $count -gt 0 ]; do
+                          do_$verb $tt
+                          let "count=count-1"
+                        done
                         ;;
                 esac
                 ;;
index 9917b0fdf127c49e76c474dddb64fea7ca39e936..48fe1b53d49149139cf36e0b602e0f0fe4d2ab3e 100644 (file)
@@ -209,7 +209,6 @@ navbar:
       - install/install-keep-balance.html.textile.liquid
     - User interface:
       - install/setup-login.html.textile.liquid
-      - install/install-sso.html.textile.liquid
       - install/install-workbench-app.html.textile.liquid
       - install/install-workbench2-app.html.textile.liquid
       - install/install-composer.html.textile.liquid
@@ -227,5 +226,4 @@ navbar:
       - install/install-postgresql.html.textile.liquid
       - install/ruby.html.textile.liquid
       - install/nginx.html.textile.liquid
-      - install/google-auth.html.textile.liquid
       - install/install-docker.html.textile.liquid
index a6a0862c4f1d1383a44a80832b42cebaafd7f569..1d6b87da62116027a96788c8fe7b73c44a269133 100644 (file)
@@ -42,7 +42,6 @@ table(table table-bordered table-condensed table-hover).
 |keepstore|✓|
 |keep-balance|✓|
 |keep-web|✓|
-|sso-provider||
 |workbench1||
 |workbench2||
 
index 6dd0d866e74c80e1f8c39fa3fbfa9c0456d08787..b684111f95300d52f23e28708a9eee1528f9e661 100644 (file)
@@ -9,37 +9,8 @@ Copyright (C) The Arvados Authors. All rights reserved.
 SPDX-License-Identifier: CC-BY-SA-3.0
 {% endcomment %}
 
-This page describes how to enable users to use more than one upstream identity provider to log into the same Arvados account.  This can be used to migrate account providers, for example, from LDAP to Google.  In order to do this, users must be able to log into both the "old" and "new" providers.
+When a user logs in to Arvados, their email address (as returned by the authentication provider) is used as the primary key for their Arvados account.
 
-h2. Configure multiple or alternate provider in SSO
+If you reconfigure Arvados to use a different authentication provider after some users have created accounts, you should either ensure the new provider returns the same email addresses as the old one, or update your Arvados users' @email@ attributes to match the email addresses returned by the new provider.
 
-In @application.yml@ for the SSO server, you can enable both @google_oauth2@ and @ldap@ providers:
-
-<pre>
-production:
-  google_oauth2_client_id: abcd
-  google_oauth2_client_secret: abcd
-
-  use_ldap:
-    title: Example LDAP
-    host: ldap.example.com
-    port: 636
-    method: ssl
-    base: "ou=Users, dc=example, dc=com"
-    uid: uid
-    username: uid
-</pre>
-
-Restart the SSO server after changing the configuration.
-
-h2. Matching on email address
-
-If the new account provider supplies an email address (primary or alternate) that matches an existing user account, the user will be logged into that account.  No further migration is necessary, and the old provider can be removed from the SSO configuration.
-
-h2. Link accounts
-
-If the new provider cannot provide matching email addresses, users will have to migrate manually by "linking accounts":{{site.baseurl}}/user/topics/link-accounts.html
-
-After linking accounts, users can use the new provider to access their existing Arvados account.
-
-Once all users have migrated, the old account provider can be removed from the SSO configuration.
+Otherwise, next time users log in, they will be given new accounts instead of logging in to their existing accounts.
index dff4fc7e34c7c7c51055db42dc98dbced6ee62df..edd92fa0ea1a117a91d60f3453dc6c3bd146ca3d 100644 (file)
@@ -38,7 +38,11 @@ h2(#master). development master (as of 2020-02-07)
 
 "Upgrading from 2.0.0":#v2_0_0
 
-None in current development master.
+h3. Removing sso-provider
+
+The SSO (single sign-on) component is deprecated and will not be supported in future releases. Existing configurations will continue to work in this release, but you should switch to one of the built-in authentication mechanisms as soon as possible. See "setting up web based login":{{site.baseurl}}/install/setup-login.html for details.
+
+After migrating your configuration, uninstall the @arvados-sso-provider@ package.
 
 h3. S3 signatures
 
index 177abd8dbe8a25c37396e88354c9b5b535a470b4..9e53775ed4abc212ead38e249e42125a3eb260b1 100644 (file)
@@ -24,7 +24,7 @@ After completing the log in and authentication process, the API server receives
 
 If a provider identifier is given, the API server searches for a matching user record.
 
-If a provider identifier is not given, no match is found, it next searches by primary email and then alternate email address.  This enables "provider migration":migrating-providers.html and "pre-activated accounts.":#pre-activated
+If a provider identifier is not given, no match is found, it next searches by primary email and then alternate email address.  This enables "provider migration":migrating-providers.html and "pre-activated accounts.":#pre-activated
 
 If no user account is found, a new user account is created with the information from the identity provider.
 
diff --git a/doc/install/google-auth.html.textile.liquid b/doc/install/google-auth.html.textile.liquid
deleted file mode 100644 (file)
index fad10ff..0000000
+++ /dev/null
@@ -1,27 +0,0 @@
----
-layout: default
-navsection: installguide
-title: Setting up Google auth
-...
-{% comment %}
-Copyright (C) The Arvados Authors. All rights reserved.
-
-SPDX-License-Identifier: CC-BY-SA-3.0
-{% endcomment %}
-
-In order to use Google for authentication, you must use the <a href="https://console.developers.google.com" target="_blank">Google Developers Console</a> to create a set of client credentials.
-
-# Go to the <a href="https://console.developers.google.com" target="_blank">Google Developers Console</a> and select or create a project; this will take you to the project page.
-# Click on *+ Enable APIs and Services*
-## Search for *People API* and click on *Enable API*.
-# Navigate back to the main "APIs & Services" page
-# On the sidebar, click on *OAuth consent screen*
-## On consent screen settings, enter your identifying details
-## Under *Authorized domains* add @example.com@
-## Click on *Save*.
-# On the sidebar, click on *Credentials*; then click on *Create credentials*&rarr;*OAuth Client ID*
-# Under *Application type* select *Web application*.
-# You must set the authorization origins.  Edit @auth.example.com@ to the appropriate hostname that you will use to access the SSO service:
-## JavaScript origin should be @https://ClusterID.example.com/@ (using Arvados-controller based login) or @https://auth.example.com/@ (for the SSO server)
-## Redirect URI should be @https://ClusterID.example.com/login@ (using Arvados-controller based login) or @https://auth.example.com/users/auth/google_oauth2/callback@ (for the SSO server)
-# Copy the values of *Client ID* and *Client secret* from the Google Developers Console and add them to the appropriate configuration.
diff --git a/doc/install/install-components.html.textile.liquid b/doc/install/install-components.html.textile.liquid
deleted file mode 100644 (file)
index 15fbe11..0000000
+++ /dev/null
@@ -1,28 +0,0 @@
----
-layout: default
-navsection: installguide
-title: Choosing which components to install
-...
-
-Arvados consists of many components, some of which may be omitted (at the cost of reduced functionality.)  It may also be helpful to review the "Arvados Architecture":{{site.baseurl}}/architecture to understand how these components interact.
-
-table(table table-bordered table-condensed).
-|\3=. *Core*|
-|"Postgres database":install-postgresql.html |Stores data for the API server.|Required.|
-|"API server":install-api-server.html |Core Arvados logic for managing users, groups, collections, containers, and enforcing permissions.|Required.|
-|\3=. *Keep (storage)*|
-|"Keepstore":install-keepstore.html |Stores content-addressed blocks in a variety of backends (local filesystem, cloud object storage).|Required.|
-|"Keepproxy":install-keepproxy.html |Gateway service to access keep servers from external networks.|Required to be able to use arv-put, arv-get, or arv-mount outside the private Arvados network.|
-|"Keep-web":install-keep-web.html |Gateway service providing read/write HTTP and WebDAV support on top of Keep.|Required to be able to download files from Keep over plain HTTP in Workbench.|
-|"Keep-balance":install-keep-balance.html |Storage cluster maintenance daemon responsible for moving blocks to their optimal server location, adjusting block replication levels, and trashing unreferenced blocks.|Required to free deleted data from underlying storage, and to ensure proper replication and block distribution (including support for storage classes).|
-|\3=. *User interface*|
-|"Single Sign On server":install-sso.html |Login server.|Required for web based login to Workbench.|
-|"Workbench":install-workbench-app.html, "Workbench2":install-workbench2-app.html |Primary graphical user interface for working with file collections and running containers.|Optional.  Depends on API server, SSO server, keep-web, websockets server.|
-|"Workflow Composer":install-composer.html |Graphical user interface for editing Common Workflow Language workflows.|Optional.  Depends on git server (arv-git-httpd).|
-|\3=. *Additional services*|
-|"Websockets server":install-ws.html |Event distribution server.|Required to view streaming container logs in Workbench.|
-|"Shell server":install-shell-server.html |Synchronize (create/delete/configure) Unix shell accounts with Arvados users.|Optional.|
-|"Git server":install-arv-git-httpd.html |Arvados-hosted git repositories, with Arvados-token based authentication.|Optional, but required by Workflow Composer.|
-|\3=. *Crunch (running containers)*|
-|"crunch-dispatch-slurm":crunch2-slurm/install-prerequisites.html |Run analysis workflows using Docker containers distributed across a SLURM cluster.|Optional if you wish to use Arvados for data management only.|
-|"Node Manager":install-nodemanager.html |Allocate and free cloud VM instances on demand based on workload.|Optional, not needed for a static SLURM cluster (such as on-premise HPC).|
index ea6ad47794d1ecf4afff02e7c1644f5c028f13d6..2ce6e36a612b701ec7b8de0494a6f71b19f4b175 100644 (file)
@@ -53,8 +53,7 @@ table(table table-bordered table-condensed).
 |"Keep-web":install-keep-web.html |Gateway service providing read/write HTTP and WebDAV support on top of Keep.|Required to access files from Workbench.|
 |"Keep-balance":install-keep-balance.html |Storage cluster maintenance daemon responsible for moving blocks to their optimal server location, adjusting block replication levels, and trashing unreferenced blocks.|Required to free deleted data from underlying storage, and to ensure proper replication and block distribution (including support for storage classes).|
 |\3=. *User interface*|
-|"Single Sign On server":install-sso.html |Web based login to Workbench.|Depends on identity provider.  Not required for Google.  Required for LDAP or standalone database.|
-|"Workbench":install-workbench-app.html, "Workbench2":install-workbench2-app.html |Primary graphical user interface for working with file collections and running containers.|Optional.  Depends on API server, SSO server, keep-web, websockets server.|
+|"Workbench":install-workbench-app.html, "Workbench2":install-workbench2-app.html |Primary graphical user interface for working with file collections and running containers.|Optional.  Depends on API server, keep-web, websockets server.|
 |"Workflow Composer":install-composer.html |Graphical user interface for editing Common Workflow Language workflows.|Optional.  Depends on git server (arv-git-httpd).|
 |\3=. *Additional services*|
 |"Websockets server":install-ws.html |Event distribution server.|Required to view streaming container logs in Workbench.|
@@ -68,9 +67,9 @@ h2(#identity). Identity provider
 
 Choose which backend you will use to authenticate users.
 
-* Google login to authenticate users with a Google account.  Note: if you only use this identity provider, login can be handled by @arvados-controller@ (recommended), and you do not need to install the Arvados Single Sign-On server (SSO).
-* LDAP login to authenticate users using the LDAP protocol, supported by many services such as OpenLDAP and Active Directory.  Supports username/password authentication.
-* Standalone SSO server user database.  Supports username/password authentication.  Supports new user sign-up.
+* Google login to authenticate users with a Google account.
+* LDAP login to authenticate users by username/password using the LDAP protocol, supported by many services such as OpenLDAP and Active Directory.
+* PAM login to authenticate users by username/password according to the PAM configuration on the controller node.
 
 h2(#storage). Storage backend
 
@@ -102,16 +101,14 @@ For a production installation, this is a reasonable starting point:
 table(table table-bordered table-condensed).
 |_. Function|_. Number of nodes|_. Recommended specs|
 |Postgres database, Arvados API server, Arvados controller, Git, Websockets, Container dispatcher|1|16+ GiB RAM, 4+ cores, fast disk for database|
-|Single Sign-On (SSO) server ^1^|1|2 GiB RAM|
 |Workbench, Keepproxy, Keep-web, Keep-balance|1|8 GiB RAM, 2+ cores|
-|Keepstore servers ^2^|2+|4 GiB RAM|
-|Compute worker nodes ^2^|0+ |Depends on workload; scaled dynamically in the cloud|
-|User shell nodes ^3^|0+|Depends on workload|
+|Keepstore servers ^1^|2+|4 GiB RAM|
+|Compute worker nodes ^1^|0+ |Depends on workload; scaled dynamically in the cloud|
+|User shell nodes ^2^|0+|Depends on workload|
 </div>
 
-^1^ May be omitted when using Google login support in @arvados-controller@
-^2^ Should be scaled up as needed
-^3^ Refers to shell nodes managed by Arvados, that provide ssh access for users to interact with Arvados at the command line.  Optional.
+^1^ Should be scaled up as needed
+^2^ Refers to shell nodes managed by Arvados, that provide ssh access for users to interact with Arvados at the command line.  Optional.
 
 {% include 'notebox_begin' %}
 For a small demo installation, it is possible to run all the Arvados services on a single node.  Special considerations for single-node installs will be noted in boxes like this.
@@ -140,7 +137,6 @@ table(table table-bordered table-condensed).
 |Arvados API|@ClusterID.example.com@|
 |Arvados Git server|git.@ClusterID.example.com@|
 |Arvados Websockets endpoint|ws.@ClusterID.example.com@|
-|Arvados SSO Server|@auth.example.com@|
 |Arvados Workbench|workbench.@ClusterID.example.com@|
 |Arvados Workbench 2|workbench2.@ClusterID.example.com@|
 |Arvados Keepproxy server|keep.@ClusterID.example.com@|
diff --git a/doc/install/install-sso.html.textile.liquid b/doc/install/install-sso.html.textile.liquid
deleted file mode 100644 (file)
index 72d4ed1..0000000
+++ /dev/null
@@ -1,237 +0,0 @@
----
-layout: default
-navsection: installguide
-title: Install the Single Sign On (SSO) server
-...
-{% comment %}
-Copyright (C) The Arvados Authors. All rights reserved.
-
-SPDX-License-Identifier: CC-BY-SA-3.0
-{% endcomment %}
-
-{% include 'notebox_begin_warning' %}
-Skip this section if you are using Google login via @arvados-controller@.
-{% include 'notebox_end' %}
-
-# "Install dependencies":#dependencies
-# "Set up database":#database-setup
-# "Update config.yml":#update-config
-# "Configure the SSO server":#create-application-yml
-# "Update Nginx configuration":#update-nginx
-# "Install arvados-sso-server":#install-packages
-# "Create arvados-server client record":#client
-# "Restart the API server and controller":#restart-api
-
-h2(#dependencies). Install dependencies
-
-# "Install PostgreSQL":install-postgresql.html
-# "Install Ruby and Bundler":ruby.html  Important!  The Single Sign On server only supports Ruby 2.3, to avoid version conflicts we recommend installing it on a different server from the API server.  When installing Ruby, ensure that you get the right version by installing the "ruby2.3" package, or by using RVM with @--ruby=2.3@
-# "Install nginx":nginx.html
-# "Install Phusion Passenger":https://www.phusionpassenger.com/library/walkthroughs/deploy/ruby/ownserver/nginx/oss/install_passenger_main.html
-
-h2(#database-setup). Set up the database
-
-{% assign service_role = "arvados_sso" %}
-{% assign service_database = "arvados_sso_production" %}
-{% assign use_contrib = false %}
-{% include 'install_postgres_database' %}
-
-Now create @/etc/arvados/sso/database.yml@
-
-<pre>
-production:
-  adapter: postgresql
-  encoding: utf8
-  database: arvados_sso_production
-  username: arvados_sso
-  password: $password
-  host: localhost
-  template: template0
-</pre>
-
-h2(#update-config). Update config.yml
-
-<pre>
-    Services:
-      SSO:
-        ExternalURL: auth.ClusterID.example.com
-    Login:
-      ProviderAppID: "arvados-server"
-      ProviderAppSecret: $app_secret
-</pre>
-
-Generate @ProviderAppSecret@:
-
-<notextile>
-<pre><code>~$ <span class="userinput">ruby -e 'puts rand(2**400).to_s(36)'</span>
-zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz
-</code></pre></notextile>
-
-h2(#create-application-yml). Configure the SSO server
-
-The SSO server runs from the @/var/www/arvados-sso/current/@ directory. The files @/var/www/arvados-sso/current/config/application.yml@ and @/var/www/arvados-sso/current/config/database.yml@ will be symlinked to the configuration files in @/etc/arvados/sso/@.
-
-The SSO server reads the @config/application.yml@ file, as well as the @config/application.defaults.yml@ file. Values in @config/application.yml@ take precedence over the defaults that are defined in @config/application.defaults.yml@. The @config/application.yml.example@ file is not read by the SSO server and is provided for installation convenience only.
-
-Consult @config/application.default.yml@ for a full list of configuration options.  Local configuration goes in @/etc/arvados/sso/application.yml@, do not edit @config/application.default.yml@.
-
-Create @/etc/arvados/sso/application.yml@ and add these keys:
-
-<pre>
-production:
-  uuid_prefix: xxxxx
-  secret_token: zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz
-</pre>
-
-h3(#uuid_prefix). uuid_prefix
-
-Most of the time, you want this to be the same as your @ClusterID@.  If not, generate a new one from the command line listed previously.
-
-h3(#secret_token). secret_token
-
-Generate a new secret token for signing cookies:
-
-<notextile>
-<pre><code>~$ <span class="userinput">ruby -e 'puts rand(2**400).to_s(36)'</span>
-zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz
-</code></pre></notextile>
-
-h3(#authentication_methods). Authentication methods
-
-Authentication methods are configured in @application.yml@.  Currently three authentication methods are supported: local accounts, LDAP, and Google.  If neither Google nor LDAP are enabled, the SSO server defaults to local user accounts.   Only one authentication mechanism should be in use at a time.  Choose your authentication method and add the listed configuration items to the @production@ section.
-
-h4(#local_accounts). Local account authentication
-
-There are two configuration options for local accounts:
-
-<pre>
-  # If true, allow new creation of new accounts in the SSO server's internal
-  # user database.
-  allow_account_registration: false
-
-  # If true, send an email confirmation before activating new accounts in the
-  # SSO server's internal user database (otherwise users are activated immediately.)
-  require_email_confirmation: false
-</pre>
-
-For more information about configuring backend support for sending email (required to send email confirmations) see "Configuring Action Mailer":http://guides.rubyonrails.org/configuring.html#configuring-action-mailer
-
-If @allow_account_registration@ is false, you may manually create local accounts on the SSO server from the Rails console.  {% include 'install_rails_command' %}
-
-Enter the following commands at the console.
-
-<notextile>
-<pre><code>:001 &gt; <span class="userinput">user = User.new(:email =&gt; "test@example.com")</span>
-:002 &gt; <span class="userinput">user.password = "passw0rd"</span>
-:003 &gt; <span class="userinput">user.save!</span>
-:004 &gt; <span class="userinput">quit</span>
-</code></pre>
-</notextile>
-
-h4(#ldap). LDAP authentication
-
-The following options are available to configure LDAP authentication.  Note that you must preserve the indentation of the fields listed under @use_ldap@.
-
-<pre>
-  use_ldap:
-    title: Example LDAP
-    host: ldap.example.com
-    port: 636
-    method: ssl
-    base: "ou=Users, dc=example, dc=com"
-    uid: uid
-    email_domain: example.com
-    #bind_dn: "some_user"
-    #password: "some_password"
-</pre>
-
-table(table).
-|_. Option|_. Description|
-|title |Title displayed to the user on the login page|
-|host  |LDAP server hostname|
-|port  |LDAP server port|
-|method|One of "plain", "ssl", "tls"|
-|base  |Directory lookup base|
-|uid   |User id field used for directory lookup|
-|email_domain|Strip off specified email domain from login and perform lookup on bare username|
-|bind_dn|If required by server, username to log with in before performing directory lookup|
-|password|If required by server, password to log with before performing directory lookup|
-
-h4(#google). Google authentication
-
-First, visit "Setting up Google auth.":google-auth.html
-
-Next, copy the values of *Client ID* and *Client secret* from the Google Developers Console into the Google section of @config/application.yml@, like this:
-
-<notextile>
-<pre><code>  # Google API tokens required for OAuth2 login.
-  google_oauth2_client_id: <span class="userinput">"---YOUR---CLIENT---ID---HERE--"-</span>
-  google_oauth2_client_secret: <span class="userinput">"---YOUR---CLIENT---SECRET---HERE--"-</span></code></pre></notextile>
-
-h2(#update-nginx). Update nginx configuration
-
-Use a text editor to create a new file @/etc/nginx/conf.d/arvados-sso.conf@ with the following configuration.  Options that need attention are marked in <span class="userinput">red</span>.
-
-<notextile>
-<pre><code>server {
-  listen       <span class="userinput">auth.ClusterID.example.com</span>:443 ssl;
-  server_name  <span class="userinput">auth.ClusterID.example.com</span>;
-
-  ssl on;
-  ssl_certificate     <span class="userinput">/YOUR/PATH/TO/cert.pem</span>;
-  ssl_certificate_key <span class="userinput">/YOUR/PATH/TO/cert.key</span>;
-
-  root   /var/www/arvados-sso/current/public;
-  index  index.html;
-
-  passenger_enabled on;
-
-  # <span class="userinput">If you are using RVM, uncomment the line below.</span>
-  # <span class="userinput">If you're using system ruby, leave it commented out.</span>
-  #passenger_ruby /usr/local/rvm/wrappers/default/ruby;
-}
-</code></pre>
-</notextile>
-
-h2(#install-packages). Install arvados-sso-server package
-
-h3. Centos 7
-
-<notextile>
-<pre><code># <span class="userinput">yum install arvados-sso-server</span>
-</code></pre>
-</notextile>
-
-h3. Debian and Ubuntu
-
-<notextile>
-<pre><code># <span class="userinput">apt-get install --no-install-recommends arvados-sso-server</span>
-</code></pre>
-</notextile>
-
-h2(#client). Create arvados-server client record
-
-{% assign railshost = "" %}
-{% assign railsdir = "/var/www/arvados-sso/current" %}
-Use @rails console@ to create a @Client@ record that will be used by the Arvados API server.  {% include 'install_rails_command' %}
-
-Enter the following commands at the console.  The values that appear after you assign @app_id@ and @app_secret@ will be copied to  @Login.ProviderAppID@ and @Login.ProviderAppSecret@ in @config.yml@.
-
-<notextile>
-<pre><code>:001 &gt; <span class="userinput">c = Client.new</span>
-:002 &gt; <span class="userinput">c.name = "joshid"</span>
-:003 &gt; <span class="userinput">c.app_id = "arvados-server"</span>
-:004 &gt; <span class="userinput">c.app_secret = "the value of Login.ProviderAppSecret"</span>
-:005 &gt; <span class="userinput">c.save!</span>
-:006 &gt; <span class="userinput">quit</span>
-</code></pre>
-</notextile>
-
-h2(#restart-api). Restart the API server and controller
-
-After adding the SSO server to the Services section, make sure the cluster config file is up to date on the API server host, and restart the API server and controller processes to ensure the changes are applied.
-
-<notextile>
-<pre><code># <span class="userinput">systemctl restart nginx arvados-controller</span>
-</code></pre>
-</notextile>
index e7b20f45a0833f5f34c41c2e913cc6abcf6ff7ac..11862a6ae6767c73ab935e8e93c4134991026789 100644 (file)
@@ -71,21 +71,12 @@ server {
 
 {% include 'restart_api' %}
 
-h2(#restart-api). Restart the API server and controller
-
-After adding the SSO server to the Services section, make sure the cluster config file is up to date on the API server host, and restart the API server and controller processes to ensure the changes are applied.
-
-<notextile>
-<pre><code># <span class="userinput">systemctl restart nginx arvados-controller</span>
-</code></pre>
-</notextile>
-
 h2(#confirm). Confirm working installation
 
 Confirm the service is listening on its assigned port and responding to requests.
 
 <notextile>
-<pre><code>~$ <span class="userinput">curl https://<span class="userinput">ws.ClusterID.example.com</span>/status.json</span>
-{"Clients":1}
+<pre><code>~$ <span class="userinput">curl https://<span class="userinput">ws.ClusterID.example.com</span>/websocket</span>
+not websocket protocol
 </code></pre>
 </notextile>
index c9e65ca7e1c5e3dc37aa5e46f40a83552f6350f8..3fe442c75b2e48e67cc9de126abeabb011fb65c4 100644 (file)
@@ -12,23 +12,62 @@ SPDX-License-Identifier: CC-BY-SA-3.0
 Select one of the following login mechanisms for your cluster.
 
 # If all users will authenticate with Google, "configure Google login":#google.
+# If all users will authenticate with an existing LDAP service, "configure LDAP":#ldap.
 # If all users will authenticate using PAM as configured on your controller node, "configure PAM":#pam.
-# If you need to enable multiple authentication methods, or your backend can't be configured as a PAM service on your controller node, "configure a separate single sign-on (SSO) server":#sso.
 
 h2(#google). Google login
 
 With this configuration, users will sign in with their Google accounts.
 
-First, visit "Setting up Google auth.":google-auth.html
+Use the <a href="https://console.developers.google.com" target="_blank">Google Developers Console</a> to create a set of client credentials.
+# Select or create a project.
+# Click *+ Enable APIs and Services*.
+#* Search for *People API* and click *Enable API*.
+#* Navigate back to the main "APIs & Services" page.
+# On the sidebar, click *OAuth consent screen*.
+#* On consent screen settings, enter your identifying details.
+#* Under *Authorized domains* add your domain (@example.com@).
+#* Click *Save*.
+# On the sidebar, click *Credentials*, then click *Create credentials*&rarr;*OAuth client ID*
+# Under *Application type* select *Web application*.
+# Add the JavaScript origin: @https://ClusterID.example.com/@
+# Add the Redirect URI: @https://ClusterID.example.com/login@
+# Copy the values of *Client ID* and *Client secret* to the @Login.Google@ section of @config.yml@.
 
-Next, copy the values of *Client ID* and *Client secret* from the Google Developers Console into @Login.GoogleClientID@ and @Login.GoogleClientSecret@ of @config.yml@:
+<pre>
+    Login:
+      Google:
+        Enable: true
+        ClientID: "0000000000000-zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz.apps.googleusercontent.com"
+        ClientSecret: "zzzzzzzzzzzzzzzzzzzzzzzz"
+</pre>
+
+h2(#ldap). LDAP
+
+With this configuration, authentication uses an external LDAP service like OpenLDAP or Active Directory.
+
+Enable LDAP authentication and provide your LDAP server's host, port, and credentials (if needed to search the directory) in @config.yml@:
 
 <pre>
     Login:
-      GoogleClientID: "0000000000000-zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz.apps.googleusercontent.com"
-      GoogleClientSecret: "zzzzzzzzzzzzzzzzzzzzzzzz"
+      LDAP:
+        Enable: true
+        URL: ldap://ldap.example.com:389
+        SearchBindUser: cn=lookupuser,dc=example,dc=com
+        SearchBindPassword: xxxxxxxx
+        SearchBase: ou=Users,dc=example,dc=com
 </pre>
 
+The email address reported by LDAP will be used as primary key for Arvados accounts. This means *users must not be able to edit their own email addresses* in the directory.
+
+Additional configuration settings are available:
+* @StartTLS@ is enabled by default.
+* @StripDomain@ and @AppendDomain@ modify the username entered by the user before searching for it in the directory.
+* @SearchAttribute@ (default @uid@) is the LDAP attribute used when searching for usernames.
+* @SearchFilters@ accepts LDAP filter expressions to control which users can log in.
+
+Check the LDAP section in the "default config file":{{site.baseurl}}/admin/config.html for more details and configuration options.
+
 h2(#pam). PAM (experimental)
 
 With this configuration, authentication is done according to the Linux PAM ("Pluggable Authentication Modules") configuration on your controller host.
@@ -37,7 +76,8 @@ Enable PAM authentication in @config.yml@:
 
 <pre>
     Login:
-      PAM: true
+      PAM:
+        Enable: true
 </pre>
 
 Check the "default config file":{{site.baseurl}}/admin/config.html for more PAM configuration options.
@@ -47,9 +87,3 @@ The default PAM configuration on most Linux systems uses the local password data
 PAM can also be configured to use different backends like LDAP. In a production environment, PAM configuration should use the service name ("arvados" by default) to set a separate policy for Arvados logins: generally, Arvados users should not have shell accounts on the controller node.
 
 For information about configuring PAM, refer to the "PAM System Administrator's Guide":http://www.linux-pam.org/Linux-PAM-html/Linux-PAM_SAG.html.
-
-h2(#sso). Separate single-sign-on (SSO) server
-
-With this configuration, Arvados passes off authentication to a separate SSO server that supports Google, LDAP, and a local password database.
-
-See "Install the Single Sign On (SSO) server":install-sso.html
diff --git a/go.mod b/go.mod
index 34b7e0779072fcf060d325ddffe77f54adffdfbf..cc5457975f54da4d6e00702a955451f104fe39d1 100644 (file)
--- a/go.mod
+++ b/go.mod
@@ -12,6 +12,7 @@ require (
        github.com/arvados/cgofuse v1.2.0-arvados1
        github.com/aws/aws-sdk-go v1.25.30
        github.com/bgentry/speakeasy v0.1.0 // indirect
+       github.com/bradleypeabody/godap v0.0.0-20170216002349-c249933bc092
        github.com/coreos/go-oidc v2.1.0+incompatible
        github.com/coreos/go-systemd v0.0.0-20180108085132-cc4f39464dc7
        github.com/dgrijalva/jwt-go v3.1.0+incompatible // indirect
@@ -25,6 +26,8 @@ require (
        github.com/fsnotify/fsnotify v1.4.9
        github.com/ghodss/yaml v1.0.0
        github.com/gliderlabs/ssh v0.2.2 // indirect
+       github.com/go-asn1-ber/asn1-ber v1.4.1 // indirect
+       github.com/go-ldap/ldap v3.0.3+incompatible
        github.com/gogo/protobuf v1.1.1
        github.com/gorilla/context v1.1.1 // indirect
        github.com/gorilla/mux v1.6.1-0.20180107155708-5bbbb5b2b572
@@ -57,6 +60,7 @@ require (
        golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45
        golang.org/x/sys v0.0.0-20191105231009-c1f44814a5cd
        google.golang.org/api v0.13.0
+       gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d // indirect
        gopkg.in/check.v1 v1.0.0-20161208181325-20d25e280405
        gopkg.in/square/go-jose.v2 v2.3.1
        gopkg.in/src-d/go-billy.v4 v4.0.1
diff --git a/go.sum b/go.sum
index 03b2f77b6d9ce62ecc5c2cf6abae2f4995bb3152..38153ce3eaa08844dd2abfb944b9318145fbeed0 100644 (file)
--- a/go.sum
+++ b/go.sum
@@ -29,6 +29,8 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
 github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
 github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQkY=
 github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
+github.com/bradleypeabody/godap v0.0.0-20170216002349-c249933bc092 h1:0Di2onNnlN5PAyWPbqlPyN45eOQ+QW/J9eqLynt4IV4=
+github.com/bradleypeabody/godap v0.0.0-20170216002349-c249933bc092/go.mod h1:8IzBjZCRSnsvM6MJMG8HNNtnzMl48H22rbJL2kRUJ0Y=
 github.com/cespare/xxhash/v2 v2.1.0 h1:yTUvW7Vhb89inJ+8irsUqiWjh8iT6sQPZiQzI6ReGkA=
 github.com/cespare/xxhash/v2 v2.1.0/go.mod h1:dgIUBU3pDso/gPgZ1osOZ0iQf77oPR28Tjxl5dIMyVM=
 github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
@@ -62,8 +64,12 @@ github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
 github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
 github.com/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0=
 github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
+github.com/go-asn1-ber/asn1-ber v1.4.1 h1:qP/QDxOtmMoJVgXHCXNzDpA0+wkgYB2x5QoLMVOciyw=
+github.com/go-asn1-ber/asn1-ber v1.4.1/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
 github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
 github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
+github.com/go-ldap/ldap v3.0.3+incompatible h1:HTeSZO8hWMS1Rgb2Ziku6b8a7qRIZZMHjsvuZyatzwk=
+github.com/go-ldap/ldap v3.0.3+incompatible/go.mod h1:qfd9rJvER9Q0/D/Sqn1DfHRoBp40uXYvFoEVrNEPqRc=
 github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
 github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
 github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
@@ -249,6 +255,8 @@ google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZi
 google.golang.org/grpc v1.20.1 h1:Hz2g2wirWK7H0qIIhGIqRGTuMwTE8HEKFnDZZ7lm9NU=
 google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
 gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
+gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d h1:TxyelI5cVkbREznMhfzycHdkp5cLA7DpE+GKjSslYhM=
+gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d/go.mod h1:cuepJuh7vyXfUyUwEgHQXw849cJrilpS5NeIjOWESAw=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20161208181325-20d25e280405 h1:829vOVxxusYHC+IqBtkX5mbKtsY9fheQiQn0MZRVLfQ=
 gopkg.in/check.v1 v1.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
index ebe39e5b223d5dd3283ebae5fb8050dfe1549ace..204f7538bad5fc268d191f9fa58a38aa5f7389a0 100644 (file)
@@ -524,54 +524,123 @@ Clusters:
         MaxUUIDEntries:       1000
 
     Login:
-      # These settings are provided by your OAuth2 provider (eg
-      # Google) used to perform upstream authentication.
-      ProviderAppID: ""
-      ProviderAppSecret: ""
-
-      # (Experimental) Authenticate with Google, bypassing the
-      # SSO-provider gateway service. Use the Google Cloud console to
-      # enable the People API (APIs and Services > Enable APIs and
-      # services > Google People API > Enable), generate a Client ID
-      # and secret (APIs and Services > Credentials > Create
-      # credentials > OAuth client ID > Web application) and add your
-      # controller's /login URL (e.g.,
-      # "https://zzzzz.example.com/login") as an authorized redirect
-      # URL.
-      #
-      # Incompatible with ForceLegacyAPI14. ProviderAppID must be
-      # blank.
-      GoogleClientID: ""
-      GoogleClientSecret: ""
-
-      # Allow users to log in to existing accounts using any verified
-      # email address listed by their Google account. If true, the
-      # Google People API must be enabled in order for Google login to
-      # work. If false, only the primary email address will be used.
-      GoogleAlternateEmailAddresses: true
-
-      # (Experimental) Use PAM to authenticate logins, using the
-      # specified PAM service name.
-      #
-      # Cannot be used in combination with OAuth2 (ProviderAppID) or
-      # Google (GoogleClientID). Cannot be used on a cluster acting as
-      # a LoginCluster.
-      PAM: false
-      PAMService: arvados
-
-      # Domain name (e.g., "example.com") to use to construct the
-      # user's email address if PAM authentication returns a username
-      # with no "@". If empty, use the PAM username as the user's
-      # email address, whether or not it contains "@".
-      #
-      # Note that the email address is used as the primary key for
-      # user records when logging in. Therefore, if you change
-      # PAMDefaultEmailDomain after the initial installation, you
-      # should also update existing user records to reflect the new
-      # domain. Otherwise, next time those users log in, they will be
-      # given new accounts instead of accessing their existing
-      # accounts.
-      PAMDefaultEmailDomain: ""
+      # One of the following mechanisms (SSO, Google, PAM, LDAP, or
+      # LoginCluster) should be enabled; see
+      # https://doc.arvados.org/install/setup-login.html
+
+      Google:
+        # Authenticate with Google.
+        Enable: false
+
+        # Use the Google Cloud console to enable the People API (APIs
+        # and Services > Enable APIs and services > Google People API
+        # > Enable), generate a Client ID and secret (APIs and
+        # Services > Credentials > Create credentials > OAuth client
+        # ID > Web application) and add your controller's /login URL
+        # (e.g., "https://zzzzz.example.com/login") as an authorized
+        # redirect URL.
+        #
+        # Incompatible with ForceLegacyAPI14. ProviderAppID must be
+        # blank.
+        ClientID: ""
+        ClientSecret: ""
+
+        # Allow users to log in to existing accounts using any verified
+        # email address listed by their Google account. If true, the
+        # Google People API must be enabled in order for Google login to
+        # work. If false, only the primary email address will be used.
+        AlternateEmailAddresses: true
+
+      PAM:
+        # (Experimental) Use PAM to authenticate users.
+        Enable: false
+
+        # PAM service name. PAM will apply the policy in the
+        # corresponding config file (e.g., /etc/pam.d/arvados) or, if
+        # there is none, the default "other" config.
+        Service: arvados
+
+        # Domain name (e.g., "example.com") to use to construct the
+        # user's email address if PAM authentication returns a
+        # username with no "@". If empty, use the PAM username as the
+        # user's email address, whether or not it contains "@".
+        #
+        # Note that the email address is used as the primary key for
+        # user records when logging in. Therefore, if you change
+        # PAMDefaultEmailDomain after the initial installation, you
+        # should also update existing user records to reflect the new
+        # domain. Otherwise, next time those users log in, they will
+        # be given new accounts instead of accessing their existing
+        # accounts.
+        DefaultEmailDomain: ""
+
+      LDAP:
+        # Use an LDAP service to authenticate users.
+        Enable: false
+
+        # Server URL, like "ldap://ldapserver.example.com:389" or
+        # "ldaps://ldapserver.example.com:636".
+        URL: "ldap://ldap:389"
+
+        # Use StartTLS upon connecting to the server.
+        StartTLS: true
+
+        # Skip TLS certificate name verification.
+        InsecureTLS: false
+
+        # Strip the @domain part if a user supplies an email-style
+        # username with this domain. If "*", strip any user-provided
+        # domain. If "", never strip the domain part. Example:
+        # "example.com"
+        StripDomain: ""
+
+        # If, after applying StripDomain, the username contains no "@"
+        # character, append this domain to form an email-style
+        # username. Example: "example.com"
+        AppendDomain: ""
+
+        # The LDAP attribute to filter on when looking up a username
+        # (after applying StripDomain and AppendDomain).
+        SearchAttribute: uid
+
+        # Bind with this username (DN or UPN) and password when
+        # looking up the user record.
+        #
+        # Example user: "cn=admin,dc=example,dc=com"
+        SearchBindUser: ""
+        SearchBindPassword: ""
+
+        # Directory base for username lookup. Example:
+        # "ou=Users,dc=example,dc=com"
+        SearchBase: ""
+
+        # Additional filters for username lookup. Special characters
+        # in assertion values must be escaped (see RFC4515). Example:
+        # "(objectClass=person)"
+        SearchFilters: ""
+
+        # LDAP attribute to use as the user's email address.
+        #
+        # Important: This must not be an attribute whose value can be
+        # edited in the directory by the users themselves. Otherwise,
+        # users can take over other users' Arvados accounts trivially
+        # (email address is the primary key for Arvados accounts.)
+        EmailAttribute: mail
+
+        # LDAP attribute to use as the preferred Arvados username. If
+        # no value is found (or this config is empty) the username
+        # originally supplied by the user will be used.
+        UsernameAttribute: uid
+
+      SSO:
+        # Authenticate with a separate SSO server. (Deprecated)
+        Enable: false
+
+        # ProviderAppID and ProviderAppSecret are generated during SSO
+        # setup; see
+        # https://doc.arvados.org/v2.0/install/install-sso.html#update-config
+        ProviderAppID: ""
+        ProviderAppSecret: ""
 
       # The cluster ID to delegate the user database.  When set,
       # logins on this cluster will be redirected to the login cluster
index bbbc9acf7534cd562c1e5dad0615fc688b07fd9b..1be7208ee38facce00e71f2cfdf07885ccffde08 100644 (file)
@@ -23,6 +23,13 @@ type deprRequestLimits struct {
 type deprCluster struct {
        RequestLimits deprRequestLimits
        NodeProfiles  map[string]nodeProfile
+       Login         struct {
+               GoogleClientID                *string
+               GoogleClientSecret            *string
+               GoogleAlternateEmailAddresses *bool
+               ProviderAppID                 *string
+               ProviderAppSecret             *string
+       }
 }
 
 type deprecatedConfig struct {
@@ -80,6 +87,34 @@ func (ldr *Loader) applyDeprecatedConfig(cfg *arvados.Config) error {
                if dst, n := &cluster.API.MaxRequestAmplification, dcluster.RequestLimits.MultiClusterRequestConcurrency; n != nil && *n != *dst {
                        *dst = *n
                }
+
+               // Google* moved to Google.*
+               if dst, n := &cluster.Login.Google.ClientID, dcluster.Login.GoogleClientID; n != nil && *n != *dst {
+                       *dst = *n
+                       if *n != "" {
+                               // In old config, non-empty ClientID meant enable
+                               cluster.Login.Google.Enable = true
+                       }
+               }
+               if dst, n := &cluster.Login.Google.ClientSecret, dcluster.Login.GoogleClientSecret; n != nil && *n != *dst {
+                       *dst = *n
+               }
+               if dst, n := &cluster.Login.Google.AlternateEmailAddresses, dcluster.Login.GoogleAlternateEmailAddresses; n != nil && *n != *dst {
+                       *dst = *n
+               }
+
+               // Provider* moved to SSO.Provider*
+               if dst, n := &cluster.Login.SSO.ProviderAppID, dcluster.Login.ProviderAppID; n != nil && *n != *dst {
+                       *dst = *n
+                       if *n != "" {
+                               // In old config, non-empty ID meant enable
+                               cluster.Login.SSO.Enable = true
+                       }
+               }
+               if dst, n := &cluster.Login.SSO.ProviderAppSecret, dcluster.Login.ProviderAppSecret; n != nil && *n != *dst {
+                       *dst = *n
+               }
+
                cfg.Clusters[id] = cluster
        }
        return nil
index 8a49c2cf8d2c7b744712f809f525a2d82afa36b5..87e26fd09672805aa5bd840b757df71778477057 100644 (file)
@@ -89,6 +89,41 @@ Clusters:
 `)
 }
 
+func (s *LoadSuite) TestDeprecatedLoginBackend(c *check.C) {
+       checkEquivalent(c, `
+Clusters:
+ z1111:
+  Login:
+   GoogleClientID: aaaa
+   GoogleClientSecret: bbbb
+   GoogleAlternateEmailAddresses: true
+`, `
+Clusters:
+ z1111:
+  Login:
+   Google:
+    Enable: true
+    ClientID: aaaa
+    ClientSecret: bbbb
+    AlternateEmailAddresses: true
+`)
+       checkEquivalent(c, `
+Clusters:
+ z1111:
+  Login:
+   ProviderAppID: aaaa
+   ProviderAppSecret: bbbb
+`, `
+Clusters:
+ z1111:
+  Login:
+   SSO:
+    Enable: true
+    ProviderAppID: aaaa
+    ProviderAppSecret: bbbb
+`)
+}
+
 func (s *LoadSuite) TestLegacyKeepWebConfig(c *check.C) {
        content := []byte(`
 {
index ded03fc3030c8811a6d12210a6c1f9b57253dfaf..26782c8ba61dca3e4b657b911b6361439522ebb6 100644 (file)
@@ -131,15 +131,34 @@ var whitelist = map[string]bool{
        "InstanceTypes.*":                              true,
        "InstanceTypes.*.*":                            true,
        "Login":                                        true,
-       "Login.GoogleClientID":                         false,
-       "Login.GoogleClientSecret":                     false,
-       "Login.GoogleAlternateEmailAddresses":          false,
-       "Login.PAM":                                    true,
-       "Login.PAMService":                             false,
-       "Login.PAMDefaultEmailDomain":                  false,
-       "Login.ProviderAppID":                          false,
-       "Login.ProviderAppSecret":                      false,
+       "Login.Google":                                 true,
+       "Login.Google.AlternateEmailAddresses":         false,
+       "Login.Google.ClientID":                        false,
+       "Login.Google.ClientSecret":                    false,
+       "Login.Google.Enable":                          true,
+       "Login.LDAP":                                   true,
+       "Login.LDAP.AppendDomain":                      false,
+       "Login.LDAP.EmailAttribute":                    false,
+       "Login.LDAP.Enable":                            true,
+       "Login.LDAP.InsecureTLS":                       false,
+       "Login.LDAP.SearchAttribute":                   false,
+       "Login.LDAP.SearchBase":                        false,
+       "Login.LDAP.SearchBindPassword":                false,
+       "Login.LDAP.SearchBindUser":                    false,
+       "Login.LDAP.SearchFilters":                     false,
+       "Login.LDAP.StartTLS":                          false,
+       "Login.LDAP.StripDomain":                       false,
+       "Login.LDAP.URL":                               false,
+       "Login.LDAP.UsernameAttribute":                 false,
        "Login.LoginCluster":                           true,
+       "Login.PAM":                                    true,
+       "Login.PAM.DefaultEmailDomain":                 false,
+       "Login.PAM.Enable":                             true,
+       "Login.PAM.Service":                            false,
+       "Login.SSO":                                    true,
+       "Login.SSO.Enable":                             true,
+       "Login.SSO.ProviderAppID":                      false,
+       "Login.SSO.ProviderAppSecret":                  false,
        "Login.RemoteTokenRefresh":                     true,
        "Mail":                                         true,
        "Mail.MailchimpAPIKey":                         false,
index 42c4374c0a13d53ef1c8610aaae59400df854ec5..ec5bc187d7625d504918d8feb8e23abf16e7018c 100644 (file)
@@ -530,54 +530,123 @@ Clusters:
         MaxUUIDEntries:       1000
 
     Login:
-      # These settings are provided by your OAuth2 provider (eg
-      # Google) used to perform upstream authentication.
-      ProviderAppID: ""
-      ProviderAppSecret: ""
-
-      # (Experimental) Authenticate with Google, bypassing the
-      # SSO-provider gateway service. Use the Google Cloud console to
-      # enable the People API (APIs and Services > Enable APIs and
-      # services > Google People API > Enable), generate a Client ID
-      # and secret (APIs and Services > Credentials > Create
-      # credentials > OAuth client ID > Web application) and add your
-      # controller's /login URL (e.g.,
-      # "https://zzzzz.example.com/login") as an authorized redirect
-      # URL.
-      #
-      # Incompatible with ForceLegacyAPI14. ProviderAppID must be
-      # blank.
-      GoogleClientID: ""
-      GoogleClientSecret: ""
-
-      # Allow users to log in to existing accounts using any verified
-      # email address listed by their Google account. If true, the
-      # Google People API must be enabled in order for Google login to
-      # work. If false, only the primary email address will be used.
-      GoogleAlternateEmailAddresses: true
-
-      # (Experimental) Use PAM to authenticate logins, using the
-      # specified PAM service name.
-      #
-      # Cannot be used in combination with OAuth2 (ProviderAppID) or
-      # Google (GoogleClientID). Cannot be used on a cluster acting as
-      # a LoginCluster.
-      PAM: false
-      PAMService: arvados
-
-      # Domain name (e.g., "example.com") to use to construct the
-      # user's email address if PAM authentication returns a username
-      # with no "@". If empty, use the PAM username as the user's
-      # email address, whether or not it contains "@".
-      #
-      # Note that the email address is used as the primary key for
-      # user records when logging in. Therefore, if you change
-      # PAMDefaultEmailDomain after the initial installation, you
-      # should also update existing user records to reflect the new
-      # domain. Otherwise, next time those users log in, they will be
-      # given new accounts instead of accessing their existing
-      # accounts.
-      PAMDefaultEmailDomain: ""
+      # One of the following mechanisms (SSO, Google, PAM, LDAP, or
+      # LoginCluster) should be enabled; see
+      # https://doc.arvados.org/install/setup-login.html
+
+      Google:
+        # Authenticate with Google.
+        Enable: false
+
+        # Use the Google Cloud console to enable the People API (APIs
+        # and Services > Enable APIs and services > Google People API
+        # > Enable), generate a Client ID and secret (APIs and
+        # Services > Credentials > Create credentials > OAuth client
+        # ID > Web application) and add your controller's /login URL
+        # (e.g., "https://zzzzz.example.com/login") as an authorized
+        # redirect URL.
+        #
+        # Incompatible with ForceLegacyAPI14. ProviderAppID must be
+        # blank.
+        ClientID: ""
+        ClientSecret: ""
+
+        # Allow users to log in to existing accounts using any verified
+        # email address listed by their Google account. If true, the
+        # Google People API must be enabled in order for Google login to
+        # work. If false, only the primary email address will be used.
+        AlternateEmailAddresses: true
+
+      PAM:
+        # (Experimental) Use PAM to authenticate users.
+        Enable: false
+
+        # PAM service name. PAM will apply the policy in the
+        # corresponding config file (e.g., /etc/pam.d/arvados) or, if
+        # there is none, the default "other" config.
+        Service: arvados
+
+        # Domain name (e.g., "example.com") to use to construct the
+        # user's email address if PAM authentication returns a
+        # username with no "@". If empty, use the PAM username as the
+        # user's email address, whether or not it contains "@".
+        #
+        # Note that the email address is used as the primary key for
+        # user records when logging in. Therefore, if you change
+        # PAMDefaultEmailDomain after the initial installation, you
+        # should also update existing user records to reflect the new
+        # domain. Otherwise, next time those users log in, they will
+        # be given new accounts instead of accessing their existing
+        # accounts.
+        DefaultEmailDomain: ""
+
+      LDAP:
+        # Use an LDAP service to authenticate users.
+        Enable: false
+
+        # Server URL, like "ldap://ldapserver.example.com:389" or
+        # "ldaps://ldapserver.example.com:636".
+        URL: "ldap://ldap:389"
+
+        # Use StartTLS upon connecting to the server.
+        StartTLS: true
+
+        # Skip TLS certificate name verification.
+        InsecureTLS: false
+
+        # Strip the @domain part if a user supplies an email-style
+        # username with this domain. If "*", strip any user-provided
+        # domain. If "", never strip the domain part. Example:
+        # "example.com"
+        StripDomain: ""
+
+        # If, after applying StripDomain, the username contains no "@"
+        # character, append this domain to form an email-style
+        # username. Example: "example.com"
+        AppendDomain: ""
+
+        # The LDAP attribute to filter on when looking up a username
+        # (after applying StripDomain and AppendDomain).
+        SearchAttribute: uid
+
+        # Bind with this username (DN or UPN) and password when
+        # looking up the user record.
+        #
+        # Example user: "cn=admin,dc=example,dc=com"
+        SearchBindUser: ""
+        SearchBindPassword: ""
+
+        # Directory base for username lookup. Example:
+        # "ou=Users,dc=example,dc=com"
+        SearchBase: ""
+
+        # Additional filters for username lookup. Special characters
+        # in assertion values must be escaped (see RFC4515). Example:
+        # "(objectClass=person)"
+        SearchFilters: ""
+
+        # LDAP attribute to use as the user's email address.
+        #
+        # Important: This must not be an attribute whose value can be
+        # edited in the directory by the users themselves. Otherwise,
+        # users can take over other users' Arvados accounts trivially
+        # (email address is the primary key for Arvados accounts.)
+        EmailAttribute: mail
+
+        # LDAP attribute to use as the preferred Arvados username. If
+        # no value is found (or this config is empty) the username
+        # originally supplied by the user will be used.
+        UsernameAttribute: uid
+
+      SSO:
+        # Authenticate with a separate SSO server. (Deprecated)
+        Enable: false
+
+        # ProviderAppID and ProviderAppSecret are generated during SSO
+        # setup; see
+        # https://doc.arvados.org/v2.0/install/install-sso.html#update-config
+        ProviderAppID: ""
+        ProviderAppSecret: ""
 
       # The cluster ID to delegate the user database.  When set,
       # logins on this cluster will be redirected to the login cluster
index 2de260fdc2493a30857894a85ebef22e7d898670..ad91bcf8028d60960044a4c578a79320587a90ed 100644 (file)
@@ -43,7 +43,8 @@ func (s *LoginSuite) TestDeferToLoginCluster(c *check.C) {
 func (s *LoginSuite) TestLogout(c *check.C) {
        s.cluster.Services.Workbench1.ExternalURL = arvados.URL{Scheme: "https", Host: "workbench1.example.com"}
        s.cluster.Services.Workbench2.ExternalURL = arvados.URL{Scheme: "https", Host: "workbench2.example.com"}
-       s.cluster.Login.GoogleClientID = "zzzzzzzzzzzzzz"
+       s.cluster.Login.Google.Enable = true
+       s.cluster.Login.Google.ClientID = "zzzzzzzzzzzzzz"
        s.addHTTPRemote(c, "zhome", &arvadostest.APIStub{})
        s.cluster.Login.LoginCluster = "zhome"
        // s.fed is already set by SetUpTest, but we need to
index f09203f72486739d467b88f28b1d6875bc2f1959..3c7ae3a2d9e5abf2773af742e0bcbc1b2213aea5 100644 (file)
@@ -167,8 +167,9 @@ func (s *HandlerSuite) TestProxyNotFound(c *check.C) {
 }
 
 func (s *HandlerSuite) TestProxyRedirect(c *check.C) {
-       s.cluster.Login.ProviderAppID = "test"
-       s.cluster.Login.ProviderAppSecret = "test"
+       s.cluster.Login.SSO.Enable = true
+       s.cluster.Login.SSO.ProviderAppID = "test"
+       s.cluster.Login.SSO.ProviderAppSecret = "test"
        req := httptest.NewRequest("GET", "https://0.0.0.0:1/login?return_to=foo", nil)
        resp := httptest.NewRecorder()
        s.handler.ServeHTTP(resp, req)
@@ -182,7 +183,8 @@ func (s *HandlerSuite) TestProxyRedirect(c *check.C) {
 }
 
 func (s *HandlerSuite) TestLogoutSSO(c *check.C) {
-       s.cluster.Login.ProviderAppID = "test"
+       s.cluster.Login.SSO.Enable = true
+       s.cluster.Login.SSO.ProviderAppID = "test"
        req := httptest.NewRequest("GET", "https://0.0.0.0:1/logout?return_to=https://example.com/foo", nil)
        resp := httptest.NewRecorder()
        s.handler.ServeHTTP(resp, req)
@@ -197,7 +199,8 @@ func (s *HandlerSuite) TestLogoutGoogle(c *check.C) {
                // Google login N/A
                return
        }
-       s.cluster.Login.GoogleClientID = "test"
+       s.cluster.Login.Google.Enable = true
+       s.cluster.Login.Google.ClientID = "test"
        req := httptest.NewRequest("GET", "https://0.0.0.0:1/logout?return_to=https://example.com/foo", nil)
        resp := httptest.NewRecorder()
        s.handler.ServeHTTP(resp, req)
index 4939b116b0bce22dfe9c3ad1c2bf79a89797b655..3bf64771d70b30d08d6c53312384071bef14a259 100644 (file)
@@ -309,6 +309,7 @@ func (s *IntegrationSuite) TestListUsers(c *check.C) {
        rootctx1, _, _ := s.rootClients("z1111")
        conn1 := s.conn("z1111")
        conn3 := s.conn("z3333")
+       userctx1, _, _ := s.userClients(rootctx1, c, conn1, "z1111", true)
 
        // Make sure LoginCluster is properly configured
        for cls := range s.testClusters {
@@ -318,7 +319,9 @@ func (s *IntegrationSuite) TestListUsers(c *check.C) {
                        check.Commentf("incorrect LoginCluster config on cluster %q", cls))
        }
        // Make sure z1111 has users with NULL usernames
-       lst, err := conn1.UserList(rootctx1, arvados.ListOptions{Limit: -1})
+       lst, err := conn1.UserList(rootctx1, arvados.ListOptions{
+               Limit: math.MaxInt64, // check that large limit works (see #16263)
+       })
        nullUsername := false
        c.Assert(err, check.IsNil)
        c.Assert(len(lst.Items), check.Not(check.Equals), 0)
@@ -328,27 +331,45 @@ func (s *IntegrationSuite) TestListUsers(c *check.C) {
                }
        }
        c.Assert(nullUsername, check.Equals, true)
+
+       user1, err := conn1.UserGetCurrent(userctx1, arvados.GetOptions{})
+       c.Assert(err, check.IsNil)
+       c.Check(user1.IsActive, check.Equals, true)
+
        // Ask for the user list on z3333 using z1111's system root token
-       _, err = conn3.UserList(rootctx1, arvados.ListOptions{Limit: -1})
-       c.Assert(err, check.IsNil, check.Commentf("getting user list: %q", err))
-}
+       lst, err = conn3.UserList(rootctx1, arvados.ListOptions{Limit: -1})
+       c.Assert(err, check.IsNil)
+       found := false
+       for _, user := range lst.Items {
+               if user.UUID == user1.UUID {
+                       c.Check(user.IsActive, check.Equals, true)
+                       found = true
+                       break
+               }
+       }
+       c.Check(found, check.Equals, true)
 
-// Test for bug #16263
-func (s *IntegrationSuite) TestListUsersWithMaxLimit(c *check.C) {
-       rootctx1, _, _ := s.rootClients("z1111")
-       conn3 := s.conn("z3333")
-       maxLimit := int64(math.MaxInt64)
+       // Deactivate user acct on z1111
+       _, err = conn1.UserUnsetup(rootctx1, arvados.GetOptions{UUID: user1.UUID})
+       c.Assert(err, check.IsNil)
 
-       // Make sure LoginCluster is properly configured
-       for cls := range s.testClusters {
-               c.Check(
-                       s.testClusters[cls].config.Clusters[cls].Login.LoginCluster,
-                       check.Equals, "z1111",
-                       check.Commentf("incorrect LoginCluster config on cluster %q", cls))
+       // Get user list from z3333, check the returned z1111 user is
+       // deactivated
+       lst, err = conn3.UserList(rootctx1, arvados.ListOptions{Limit: -1})
+       c.Assert(err, check.IsNil)
+       found = false
+       for _, user := range lst.Items {
+               if user.UUID == user1.UUID {
+                       c.Check(user.IsActive, check.Equals, false)
+                       found = true
+                       break
+               }
        }
+       c.Check(found, check.Equals, true)
 
-       // Ask for the user list on z3333 using z1111's system root token and
-       // limit: max int64 value.
-       _, err := conn3.UserList(rootctx1, arvados.ListOptions{Limit: maxLimit})
-       c.Assert(err, check.IsNil, check.Commentf("getting user list: %q", err))
+       // Deactivated user can see is_active==false via "get current
+       // user" API
+       user1, err = conn3.UserGetCurrent(userctx1, arvados.GetOptions{})
+       c.Assert(err, check.IsNil)
+       c.Check(user1.IsActive, check.Equals, false)
 }
index ae59849993346afaf6eddf37dd53cfa08c5cce5e..0fd0a9ad2348045c1abd85b3f47a54a5d25dc202 100644 (file)
@@ -8,8 +8,11 @@ import (
        "context"
        "errors"
        "net/http"
+       "net/url"
 
+       "git.arvados.org/arvados.git/lib/controller/rpc"
        "git.arvados.org/arvados.git/sdk/go/arvados"
+       "git.arvados.org/arvados.git/sdk/go/auth"
        "git.arvados.org/arvados.git/sdk/go/httpserver"
 )
 
@@ -20,19 +23,22 @@ type loginController interface {
 }
 
 func chooseLoginController(cluster *arvados.Cluster, railsProxy *railsProxy) loginController {
-       wantGoogle := cluster.Login.GoogleClientID != ""
-       wantSSO := cluster.Login.ProviderAppID != ""
-       wantPAM := cluster.Login.PAM
+       wantGoogle := cluster.Login.Google.Enable
+       wantSSO := cluster.Login.SSO.Enable
+       wantPAM := cluster.Login.PAM.Enable
+       wantLDAP := cluster.Login.LDAP.Enable
        switch {
-       case wantGoogle && !wantSSO && !wantPAM:
+       case wantGoogle && !wantSSO && !wantPAM && !wantLDAP:
                return &googleLoginController{Cluster: cluster, RailsProxy: railsProxy}
-       case !wantGoogle && wantSSO && !wantPAM:
+       case !wantGoogle && wantSSO && !wantPAM && !wantLDAP:
                return &ssoLoginController{railsProxy}
-       case !wantGoogle && !wantSSO && wantPAM:
+       case !wantGoogle && !wantSSO && wantPAM && !wantLDAP:
                return &pamLoginController{Cluster: cluster, RailsProxy: railsProxy}
+       case !wantGoogle && !wantSSO && !wantPAM && wantLDAP:
+               return &ldapLoginController{Cluster: cluster, RailsProxy: railsProxy}
        default:
                return errorLoginController{
-                       error: errors.New("configuration problem: exactly one of Login.GoogleClientID, Login.ProviderAppID, or Login.PAM must be configured"),
+                       error: errors.New("configuration problem: exactly one of Login.Google, Login.SSO, Login.PAM, and Login.LDAP must be enabled"),
                }
        }
 }
@@ -68,3 +74,23 @@ func noopLogout(cluster *arvados.Cluster, opts arvados.LogoutOptions) (arvados.L
        }
        return arvados.LogoutResponse{RedirectLocation: target}, nil
 }
+
+func createAPIClientAuthorization(ctx context.Context, conn *rpc.Conn, rootToken string, authinfo rpc.UserSessionAuthInfo) (arvados.APIClientAuthorization, error) {
+       ctxRoot := auth.NewContext(ctx, &auth.Credentials{Tokens: []string{rootToken}})
+       resp, err := conn.UserSessionCreate(ctxRoot, rpc.UserSessionCreateOptions{
+               // Send a fake ReturnTo value instead of the caller's
+               // opts.ReturnTo. We won't follow the resulting
+               // redirect target anyway.
+               ReturnTo: ",https://none.invalid",
+               AuthInfo: authinfo,
+       })
+       if err != nil {
+               return arvados.APIClientAuthorization{}, err
+       }
+       target, err := url.Parse(resp.RedirectLocation)
+       if err != nil {
+               return arvados.APIClientAuthorization{}, err
+       }
+       token := target.Query().Get("api_token")
+       return conn.APIClientAuthorizationCurrent(auth.NewContext(ctx, auth.NewCredentials(token)), arvados.GetOptions{})
+}
index bf1754c158968e40c6cb7d32a31d55ab4c83fcde..144b04c46d7ee6ab865eff24a7acc06db08f5dd9 100644 (file)
@@ -71,8 +71,8 @@ func (ctrl *googleLoginController) Login(ctx context.Context, opts arvados.Login
                return loginError(fmt.Errorf("error making redirect URL: %s", err))
        }
        conf := &oauth2.Config{
-               ClientID:     ctrl.Cluster.Login.GoogleClientID,
-               ClientSecret: ctrl.Cluster.Login.GoogleClientSecret,
+               ClientID:     ctrl.Cluster.Login.Google.ClientID,
+               ClientSecret: ctrl.Cluster.Login.Google.ClientSecret,
                Endpoint:     provider.Endpoint(),
                Scopes:       []string{oidc.ScopeOpenID, "profile", "email"},
                RedirectURL:  redirURL.String(),
@@ -162,7 +162,7 @@ func (ctrl *googleLoginController) getAuthInfo(ctx context.Context, cluster *arv
                ret.Email = claims.Email
        }
 
-       if !ctrl.Cluster.Login.GoogleAlternateEmailAddresses {
+       if !ctrl.Cluster.Login.Google.AlternateEmailAddresses {
                if ret.Email == "" {
                        return nil, fmt.Errorf("cannot log in with unverified email address %q", claims.Email)
                }
index 9e16e2e90439a8ab7767930b6c701fe6d6ab604a..495fbb69b31e5c659b4476c772303658e46592c1 100644 (file)
@@ -146,10 +146,10 @@ func (s *LoginSuite) SetUpTest(c *check.C) {
 
        cfg, err := config.NewLoader(nil, ctxlog.TestLogger(c)).Load()
        s.cluster, err = cfg.GetCluster("")
-       s.cluster.Login.ProviderAppID = ""
-       s.cluster.Login.ProviderAppSecret = ""
-       s.cluster.Login.GoogleClientID = "test%client$id"
-       s.cluster.Login.GoogleClientSecret = "test#client/secret"
+       s.cluster.Login.SSO.Enable = false
+       s.cluster.Login.Google.Enable = true
+       s.cluster.Login.Google.ClientID = "test%client$id"
+       s.cluster.Login.Google.ClientSecret = "test#client/secret"
        s.cluster.Users.PreferDomainForUsername = "PreferDomainForUsername.example.com"
        c.Assert(err, check.IsNil)
 
@@ -227,7 +227,7 @@ func (s *LoginSuite) setupPeopleAPIError(c *check.C) {
 }
 
 func (s *LoginSuite) TestGoogleLogin_PeopleAPIDisabled(c *check.C) {
-       s.cluster.Login.GoogleAlternateEmailAddresses = false
+       s.cluster.Login.Google.AlternateEmailAddresses = false
        s.authEmail = "joe.smith@primary.example.com"
        s.setupPeopleAPIError(c)
        state := s.startLogin(c)
diff --git a/lib/controller/localdb/login_ldap.go b/lib/controller/localdb/login_ldap.go
new file mode 100644 (file)
index 0000000..6c430d6
--- /dev/null
@@ -0,0 +1,152 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+package localdb
+
+import (
+       "context"
+       "crypto/tls"
+       "errors"
+       "fmt"
+       "net"
+       "net/http"
+       "strings"
+
+       "git.arvados.org/arvados.git/lib/controller/rpc"
+       "git.arvados.org/arvados.git/sdk/go/arvados"
+       "git.arvados.org/arvados.git/sdk/go/ctxlog"
+       "git.arvados.org/arvados.git/sdk/go/httpserver"
+       "github.com/go-ldap/ldap"
+)
+
+type ldapLoginController struct {
+       Cluster    *arvados.Cluster
+       RailsProxy *railsProxy
+}
+
+func (ctrl *ldapLoginController) Logout(ctx context.Context, opts arvados.LogoutOptions) (arvados.LogoutResponse, error) {
+       return noopLogout(ctrl.Cluster, opts)
+}
+
+func (ctrl *ldapLoginController) Login(ctx context.Context, opts arvados.LoginOptions) (arvados.LoginResponse, error) {
+       return arvados.LoginResponse{}, errors.New("interactive login is not available")
+}
+
+func (ctrl *ldapLoginController) UserAuthenticate(ctx context.Context, opts arvados.UserAuthenticateOptions) (arvados.APIClientAuthorization, error) {
+       log := ctxlog.FromContext(ctx)
+       conf := ctrl.Cluster.Login.LDAP
+       errFailed := httpserver.ErrorWithStatus(fmt.Errorf("LDAP: Authentication failure (with username %q and password)", opts.Username), http.StatusUnauthorized)
+
+       if conf.SearchAttribute == "" {
+               return arvados.APIClientAuthorization{}, errors.New("config error: SearchAttribute is blank")
+       }
+       if opts.Password == "" {
+               log.WithField("username", opts.Username).Error("refusing to authenticate with empty password")
+               return arvados.APIClientAuthorization{}, errFailed
+       }
+
+       log = log.WithField("URL", conf.URL.String())
+       l, err := ldap.DialURL(conf.URL.String())
+       if err != nil {
+               log.WithError(err).Error("ldap connection failed")
+               return arvados.APIClientAuthorization{}, err
+       }
+       defer l.Close()
+
+       if conf.StartTLS {
+               var tlsconfig tls.Config
+               if conf.InsecureTLS {
+                       tlsconfig.InsecureSkipVerify = true
+               } else {
+                       if host, _, err := net.SplitHostPort(conf.URL.Host); err != nil {
+                               // Assume SplitHostPort error means
+                               // port was not specified
+                               tlsconfig.ServerName = conf.URL.Host
+                       } else {
+                               tlsconfig.ServerName = host
+                       }
+               }
+               err = l.StartTLS(&tlsconfig)
+               if err != nil {
+                       log.WithError(err).Error("ldap starttls failed")
+                       return arvados.APIClientAuthorization{}, err
+               }
+       }
+
+       username := opts.Username
+       if at := strings.Index(username, "@"); at >= 0 {
+               if conf.StripDomain == "*" || strings.ToLower(conf.StripDomain) == strings.ToLower(username[at+1:]) {
+                       username = username[:at]
+               }
+       }
+       if conf.AppendDomain != "" && !strings.Contains(username, "@") {
+               username = username + "@" + conf.AppendDomain
+       }
+
+       if conf.SearchBindUser != "" {
+               err = l.Bind(conf.SearchBindUser, conf.SearchBindPassword)
+               if err != nil {
+                       log.WithError(err).WithField("user", conf.SearchBindUser).Error("ldap authentication failed")
+                       return arvados.APIClientAuthorization{}, err
+               }
+       }
+
+       search := fmt.Sprintf("(%s=%s)", ldap.EscapeFilter(conf.SearchAttribute), ldap.EscapeFilter(username))
+       if conf.SearchFilters != "" {
+               search = fmt.Sprintf("(&%s%s)", conf.SearchFilters, search)
+       }
+       log = log.WithField("search", search)
+       req := ldap.NewSearchRequest(
+               conf.SearchBase,
+               ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 1, 0, false,
+               search,
+               []string{"DN", "givenName", "SN", conf.EmailAttribute, conf.UsernameAttribute},
+               nil)
+       resp, err := l.Search(req)
+       if ldap.IsErrorWithCode(err, ldap.LDAPResultNoResultsReturned) ||
+               ldap.IsErrorWithCode(err, ldap.LDAPResultNoSuchObject) ||
+               (err == nil && len(resp.Entries) == 0) {
+               log.WithError(err).Info("ldap lookup returned no results")
+               return arvados.APIClientAuthorization{}, errFailed
+       } else if err != nil {
+               log.WithError(err).Error("ldap lookup failed")
+               return arvados.APIClientAuthorization{}, err
+       }
+       userdn := resp.Entries[0].DN
+       if userdn == "" {
+               log.Warn("refusing to authenticate with empty dn")
+               return arvados.APIClientAuthorization{}, errFailed
+       }
+       log = log.WithField("DN", userdn)
+
+       attrs := map[string]string{}
+       for _, attr := range resp.Entries[0].Attributes {
+               if attr == nil || len(attr.Values) == 0 {
+                       continue
+               }
+               attrs[strings.ToLower(attr.Name)] = attr.Values[0]
+       }
+       log.WithField("attrs", attrs).Debug("ldap search succeeded")
+
+       // Now that we have the DN, try authenticating.
+       err = l.Bind(userdn, opts.Password)
+       if err != nil {
+               log.WithError(err).Info("ldap user authentication failed")
+               return arvados.APIClientAuthorization{}, errFailed
+       }
+       log.Debug("ldap authentication succeeded")
+
+       email := attrs[strings.ToLower(conf.EmailAttribute)]
+       if email == "" {
+               log.Errorf("ldap returned no email address in %q attribute", conf.EmailAttribute)
+               return arvados.APIClientAuthorization{}, errors.New("authentication succeeded but ldap returned no email address")
+       }
+
+       return createAPIClientAuthorization(ctx, ctrl.RailsProxy, ctrl.Cluster.SystemRootToken, rpc.UserSessionAuthInfo{
+               Email:     email,
+               FirstName: attrs["givenname"],
+               LastName:  attrs["sn"],
+               Username:  attrs[strings.ToLower(conf.UsernameAttribute)],
+       })
+}
diff --git a/lib/controller/localdb/login_ldap_docker_test.go b/lib/controller/localdb/login_ldap_docker_test.go
new file mode 100644 (file)
index 0000000..79b5f16
--- /dev/null
@@ -0,0 +1,48 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+package localdb
+
+import (
+       "os"
+       "os/exec"
+       "testing"
+
+       check "gopkg.in/check.v1"
+)
+
+func haveDocker() bool {
+       _, err := exec.Command("docker", "info").CombinedOutput()
+       return err == nil
+}
+
+func (s *LDAPSuite) TestLoginLDAPViaPAM(c *check.C) {
+       if testing.Short() {
+               c.Skip("skipping docker test in short mode")
+       }
+       if !haveDocker() {
+               c.Skip("skipping docker test because docker is not available")
+       }
+       cmd := exec.Command("bash", "login_ldap_docker_test.sh")
+       cmd.Stdout = os.Stderr
+       cmd.Stderr = os.Stderr
+       cmd.Env = append(os.Environ(), "config_method=pam")
+       err := cmd.Run()
+       c.Check(err, check.IsNil)
+}
+
+func (s *LDAPSuite) TestLoginLDAPBuiltin(c *check.C) {
+       if testing.Short() {
+               c.Skip("skipping docker test in short mode")
+       }
+       if !haveDocker() {
+               c.Skip("skipping docker test because docker is not available")
+       }
+       cmd := exec.Command("bash", "login_ldap_docker_test.sh")
+       cmd.Stdout = os.Stderr
+       cmd.Stderr = os.Stderr
+       cmd.Env = append(os.Environ(), "config_method=ldap")
+       err := cmd.Run()
+       c.Check(err, check.IsNil)
+}
similarity index 63%
rename from lib/controller/localdb/login_pam_docker_test.sh
rename to lib/controller/localdb/login_ldap_docker_test.sh
index b8f281bc2e69dfd80f3c3451b161330bbc0a2d47..4e0679f620bf6b4ec67d8fc118b66db5ef3332ac 100755 (executable)
@@ -2,9 +2,9 @@
 
 # This script demonstrates using LDAP for Arvados user authentication.
 #
-# It configures pam_ldap(5) and arvados controller in a docker
-# container, with pam_ldap configured to authenticate against an
-# OpenLDAP server in a second docker container.
+# It configures arvados controller in a docker container, optionally
+# with pam_ldap(5) configured to authenticate against an OpenLDAP
+# server in a second docker container.
 #
 # After adding a "foo" user entry, it uses curl to check that the
 # Arvados controller's login endpoint accepts the "foo" account
@@ -24,6 +24,15 @@ if [[ -n ${ARVADOS_DEBUG} ]]; then
     set -x
 fi
 
+case "${config_method}" in
+    pam | ldap)
+        ;;
+    *)
+        echo >&2 "\$config_method env var must be 'pam' or 'ldap'"
+        exit 1
+        ;;
+esac
+
 hostname="$(hostname)"
 tmpdir="$(mktemp -d)"
 cleanup() {
@@ -86,15 +95,38 @@ Clusters:
         ExternalURL: http://0.0.0.0:9999/
         InternalURLs:
           "http://0.0.0.0:9999/": {}
-    Login:
-      PAM: true
-      # Without this magic PAMDefaultEmailDomain, inserted users would
-      # prevent subsequent database/reset from working (see
-      # database_controller.rb).
-      PAMDefaultEmailDomain: example.com
     SystemLogs:
       LogLevel: debug
 EOF
+case "${config_method}" in
+    pam)
+        setup_pam_ldap="apt update && DEBIAN_FRONTEND=noninteractive apt install -y ldap-utils libpam-ldap && pam-auth-update --package /usr/share/pam-configs/ldap"
+        cat >>"${tmpdir}/zzzzz.yml" <<EOF
+    Login:
+      PAM:
+        Enable: true
+        # Without this specific DefaultEmailDomain, inserted users
+        # would prevent subsequent database/reset from working (see
+        # database_controller.rb).
+        DefaultEmailDomain: example.com
+EOF
+        ;;
+    ldap)
+        setup_pam_ldap=""
+        cat >>"${tmpdir}/zzzzz.yml" <<EOF
+    Login:
+      LDAP:
+        Enable: true
+        URL: ${ldapurl}
+        StartTLS: false
+        SearchBase: dc=example,dc=org
+        SearchBindUser: cn=admin,dc=example,dc=org
+        SearchBindPassword: admin
+EOF
+            ;;
+esac
+
+cat >&2 "${tmpdir}/zzzzz.yml"
 
 cat >"${tmpdir}/pam_ldap.conf" <<EOF
 base dc=example,dc=org
@@ -113,12 +145,12 @@ cn: bar
 gidNumber: 11111
 description: "Example group 'bar'"
 
-dn: uid=foo,dc=example,dc=org
-uid: foo
-cn: foo
+dn: uid=foo-bar,dc=example,dc=org
+uid: foo-bar
+cn: "Foo Bar"
 givenName: Foo
 sn: Bar
-mail: foobar@example.org
+mail: foo-bar-baz@example.com
 objectClass: inetOrgPerson
 objectClass: posixAccount
 objectClass: top
@@ -130,11 +162,11 @@ shadowLastChange: 10701
 loginShell: /bin/bash
 uidNumber: 11111
 gidNumber: 11111
-homeDirectory: /home/foo
+homeDirectory: /home/foo-bar
 userPassword: ${passwordhash}
 EOF
 
-echo >&2 "Adding example user entry user=foo pass=secret (retrying until server comes up)"
+echo >&2 "Adding example user entry user=foo-bar pass=secret (retrying until server comes up)"
 docker run --rm --entrypoint= \
        -v "${tmpdir}/add_example_user.ldif":/add_example_user.ldif:ro \
        osixia/openldap:1.3.0 \
@@ -152,7 +184,7 @@ docker run --detach --rm --name=${ctrlctr} \
        -v "${tmpdir}/zzzzz.yml":/etc/arvados/config.yml:ro \
        -v $(realpath "${PWD}/../../.."):/arvados:ro \
        debian:10 \
-       bash -c "apt update && DEBIAN_FRONTEND=noninteractive apt install -y ldap-utils libpam-ldap && pam-auth-update --package /usr/share/pam-configs/ldap && arvados-server controller"
+       bash -c "${setup_pam_ldap:-true} && arvados-server controller"
 docker logs --follow ${ctrlctr} 2>$debug >$debug &
 ctrlhostport=$(docker port ${ctrlctr} 9999/tcp)
 
@@ -178,16 +210,42 @@ check_contains() {
     fi
 }
 
+set +x
+
 echo >&2 "Testing authentication failure"
-resp="$(curl -s --include -d username=foo -d password=nosecret "http://${ctrlhostport}/arvados/v1/users/authenticate" | tee $debug)"
+resp="$(set -x; curl -s --include -d username=foo-bar -d password=nosecret "http://${ctrlhostport}/arvados/v1/users/authenticate" | tee $debug)"
 check_contains "${resp}" "HTTP/1.1 401"
-check_contains "${resp}" '{"errors":["PAM: Authentication failure (with username \"foo\" and password)"]}'
+if [[ "${config_method}" = ldap ]]; then
+    check_contains "${resp}" '{"errors":["LDAP: Authentication failure (with username \"foo-bar\" and password)"]}'
+else
+    check_contains "${resp}" '{"errors":["PAM: Authentication failure (with username \"foo-bar\" and password)"]}'
+fi
 
 echo >&2 "Testing authentication success"
-resp="$(curl -s --include -d username=foo -d password=secret "http://${ctrlhostport}/arvados/v1/users/authenticate" | tee $debug)"
+resp="$(set -x; curl -s --include -d username=foo-bar -d password=secret "http://${ctrlhostport}/arvados/v1/users/authenticate" | tee $debug)"
 check_contains "${resp}" "HTTP/1.1 200"
 check_contains "${resp}" '"api_token":"'
 check_contains "${resp}" '"scopes":["all"]'
 check_contains "${resp}" '"uuid":"zzzzz-gj3su-'
 
+secret="${resp##*api_token\":\"}"
+secret="${secret%%\"*}"
+uuid="${resp##*uuid\":\"}"
+uuid="${uuid%%\"*}"
+token="v2/$uuid/$secret"
+echo >&2 "New token is ${token}"
+
+resp="$(set -x; curl -s --include -H "Authorization: Bearer ${token}" "http://${ctrlhostport}/arvados/v1/users/current" | tee $debug)"
+check_contains "${resp}" "HTTP/1.1 200"
+if [[ "${config_method}" = ldap ]]; then
+    # user fields come from LDAP attributes
+    check_contains "${resp}" '"first_name":"Foo"'
+    check_contains "${resp}" '"last_name":"Bar"'
+    check_contains "${resp}" '"username":"foobar"' # "-" removed by rails api
+    check_contains "${resp}" '"email":"foo-bar-baz@example.com"'
+else
+    # PAMDefaultEmailDomain
+    check_contains "${resp}" '"email":"foo-bar@example.com"'
+fi
+
 cleanup
diff --git a/lib/controller/localdb/login_ldap_test.go b/lib/controller/localdb/login_ldap_test.go
new file mode 100644 (file)
index 0000000..9a8f83f
--- /dev/null
@@ -0,0 +1,133 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+package localdb
+
+import (
+       "context"
+       "encoding/json"
+       "net"
+       "net/http"
+
+       "git.arvados.org/arvados.git/lib/config"
+       "git.arvados.org/arvados.git/lib/controller/railsproxy"
+       "git.arvados.org/arvados.git/sdk/go/arvados"
+       "git.arvados.org/arvados.git/sdk/go/arvadostest"
+       "git.arvados.org/arvados.git/sdk/go/auth"
+       "git.arvados.org/arvados.git/sdk/go/ctxlog"
+       "github.com/bradleypeabody/godap"
+       check "gopkg.in/check.v1"
+)
+
+var _ = check.Suite(&LDAPSuite{})
+
+type LDAPSuite struct {
+       cluster *arvados.Cluster
+       ctrl    *ldapLoginController
+       ldap    *godap.LDAPServer // fake ldap server that accepts auth goodusername/goodpassword
+}
+
+func (s *LDAPSuite) TearDownSuite(c *check.C) {
+       // Undo any changes/additions to the user database so they
+       // don't affect subsequent tests.
+       arvadostest.ResetEnv()
+       c.Check(arvados.NewClientFromEnv().RequestAndDecode(nil, "POST", "database/reset", nil, nil), check.IsNil)
+}
+
+func (s *LDAPSuite) SetUpSuite(c *check.C) {
+       cfg, err := config.NewLoader(nil, ctxlog.TestLogger(c)).Load()
+       c.Assert(err, check.IsNil)
+       s.cluster, err = cfg.GetCluster("")
+       c.Assert(err, check.IsNil)
+
+       ln, err := net.Listen("tcp", "127.0.0.1:0")
+       s.ldap = &godap.LDAPServer{
+               Listener: ln,
+               Handlers: []godap.LDAPRequestHandler{
+                       &godap.LDAPBindFuncHandler{
+                               LDAPBindFunc: func(binddn string, bindpw []byte) bool {
+                                       return binddn == "cn=goodusername,dc=example,dc=com" && string(bindpw) == "goodpassword"
+                               },
+                       },
+                       &godap.LDAPSimpleSearchFuncHandler{
+                               LDAPSimpleSearchFunc: func(req *godap.LDAPSimpleSearchRequest) []*godap.LDAPSimpleSearchResultEntry {
+                                       if req.FilterAttr != "uid" || req.BaseDN != "dc=example,dc=com" {
+                                               return []*godap.LDAPSimpleSearchResultEntry{}
+                                       }
+                                       return []*godap.LDAPSimpleSearchResultEntry{
+                                               &godap.LDAPSimpleSearchResultEntry{
+                                                       DN: "cn=" + req.FilterValue + "," + req.BaseDN,
+                                                       Attrs: map[string]interface{}{
+                                                               "SN":   req.FilterValue,
+                                                               "CN":   req.FilterValue,
+                                                               "uid":  req.FilterValue,
+                                                               "mail": req.FilterValue + "@example.com",
+                                                       },
+                                               },
+                                       }
+                               },
+                       },
+               },
+       }
+       go func() {
+               ctxlog.TestLogger(c).Print(s.ldap.Serve())
+       }()
+
+       s.cluster.Login.LDAP.Enable = true
+       err = json.Unmarshal([]byte(`"ldap://`+ln.Addr().String()+`"`), &s.cluster.Login.LDAP.URL)
+       s.cluster.Login.LDAP.StartTLS = false
+       s.cluster.Login.LDAP.SearchBindUser = "cn=goodusername,dc=example,dc=com"
+       s.cluster.Login.LDAP.SearchBindPassword = "goodpassword"
+       s.cluster.Login.LDAP.SearchBase = "dc=example,dc=com"
+       c.Assert(err, check.IsNil)
+       s.ctrl = &ldapLoginController{
+               Cluster:    s.cluster,
+               RailsProxy: railsproxy.NewConn(s.cluster),
+       }
+}
+
+func (s *LDAPSuite) TestLoginSuccess(c *check.C) {
+       resp, err := s.ctrl.UserAuthenticate(context.Background(), arvados.UserAuthenticateOptions{
+               Username: "goodusername",
+               Password: "goodpassword",
+       })
+       c.Check(err, check.IsNil)
+       c.Check(resp.APIToken, check.Not(check.Equals), "")
+       c.Check(resp.UUID, check.Matches, `zzzzz-gj3su-.*`)
+       c.Check(resp.Scopes, check.DeepEquals, []string{"all"})
+
+       ctx := auth.NewContext(context.Background(), &auth.Credentials{Tokens: []string{"v2/" + resp.UUID + "/" + resp.APIToken}})
+       user, err := railsproxy.NewConn(s.cluster).UserGetCurrent(ctx, arvados.GetOptions{})
+       c.Check(err, check.IsNil)
+       c.Check(user.Email, check.Equals, "goodusername@example.com")
+       c.Check(user.Username, check.Equals, "goodusername")
+}
+
+func (s *LDAPSuite) TestLoginFailure(c *check.C) {
+       // search returns no results
+       s.cluster.Login.LDAP.SearchBase = "dc=example,dc=invalid"
+       resp, err := s.ctrl.UserAuthenticate(context.Background(), arvados.UserAuthenticateOptions{
+               Username: "goodusername",
+               Password: "goodpassword",
+       })
+       c.Check(err, check.ErrorMatches, `LDAP: Authentication failure \(with username "goodusername" and password\)`)
+       hs, ok := err.(interface{ HTTPStatus() int })
+       if c.Check(ok, check.Equals, true) {
+               c.Check(hs.HTTPStatus(), check.Equals, http.StatusUnauthorized)
+       }
+       c.Check(resp.APIToken, check.Equals, "")
+
+       // search returns result, but auth fails
+       s.cluster.Login.LDAP.SearchBase = "dc=example,dc=com"
+       resp, err = s.ctrl.UserAuthenticate(context.Background(), arvados.UserAuthenticateOptions{
+               Username: "badusername",
+               Password: "badpassword",
+       })
+       c.Check(err, check.ErrorMatches, `LDAP: Authentication failure \(with username "badusername" and password\)`)
+       hs, ok = err.(interface{ HTTPStatus() int })
+       if c.Check(ok, check.Equals, true) {
+               c.Check(hs.HTTPStatus(), check.Equals, http.StatusUnauthorized)
+       }
+       c.Check(resp.APIToken, check.Equals, "")
+}
index 01dfc1379d3064b06ad7a3e7760d60250cc00a52..2447713a2cf453ea05cfc29e2c643fa0713848a9 100644 (file)
@@ -9,12 +9,10 @@ import (
        "errors"
        "fmt"
        "net/http"
-       "net/url"
        "strings"
 
        "git.arvados.org/arvados.git/lib/controller/rpc"
        "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/msteinert/pam"
@@ -37,7 +35,7 @@ func (ctrl *pamLoginController) Login(ctx context.Context, opts arvados.LoginOpt
 func (ctrl *pamLoginController) UserAuthenticate(ctx context.Context, opts arvados.UserAuthenticateOptions) (arvados.APIClientAuthorization, error) {
        errorMessage := ""
        sentPassword := false
-       tx, err := pam.StartFunc(ctrl.Cluster.Login.PAMService, opts.Username, func(style pam.Style, message string) (string, error) {
+       tx, err := pam.StartFunc(ctrl.Cluster.Login.PAM.Service, opts.Username, func(style pam.Style, message string) (string, error) {
                ctxlog.FromContext(ctx).Debugf("pam conversation: style=%v message=%q", style, message)
                switch style {
                case pam.ErrorMsg:
@@ -82,28 +80,15 @@ func (ctrl *pamLoginController) UserAuthenticate(ctx context.Context, opts arvad
                return arvados.APIClientAuthorization{}, err
        }
        email := user
-       if domain := ctrl.Cluster.Login.PAMDefaultEmailDomain; domain != "" && !strings.Contains(email, "@") {
+       if domain := ctrl.Cluster.Login.PAM.DefaultEmailDomain; domain != "" && !strings.Contains(email, "@") {
                email = email + "@" + domain
        }
-       ctxlog.FromContext(ctx).WithFields(logrus.Fields{"user": user, "email": email}).Debug("pam authentication succeeded")
-       ctxRoot := auth.NewContext(ctx, &auth.Credentials{Tokens: []string{ctrl.Cluster.SystemRootToken}})
-       resp, err := ctrl.RailsProxy.UserSessionCreate(ctxRoot, rpc.UserSessionCreateOptions{
-               // Send a fake ReturnTo value instead of the caller's
-               // opts.ReturnTo. We won't follow the resulting
-               // redirect target anyway.
-               ReturnTo: ",https://none.invalid",
-               AuthInfo: rpc.UserSessionAuthInfo{
-                       Username: user,
-                       Email:    email,
-               },
+       ctxlog.FromContext(ctx).WithFields(logrus.Fields{
+               "user":  user,
+               "email": email,
+       }).Debug("pam authentication succeeded")
+       return createAPIClientAuthorization(ctx, ctrl.RailsProxy, ctrl.Cluster.SystemRootToken, rpc.UserSessionAuthInfo{
+               Username: user,
+               Email:    email,
        })
-       if err != nil {
-               return arvados.APIClientAuthorization{}, err
-       }
-       target, err := url.Parse(resp.RedirectLocation)
-       if err != nil {
-               return arvados.APIClientAuthorization{}, err
-       }
-       token := target.Query().Get("api_token")
-       return ctrl.RailsProxy.APIClientAuthorizationCurrent(auth.NewContext(ctx, auth.NewCredentials(token)), arvados.GetOptions{})
 }
diff --git a/lib/controller/localdb/login_pam_docker_test.go b/lib/controller/localdb/login_pam_docker_test.go
deleted file mode 100644 (file)
index 8a02b2c..0000000
+++ /dev/null
@@ -1,23 +0,0 @@
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-// Skip this slow test unless invoked as "go test -tags docker".
-// +build docker
-
-package localdb
-
-import (
-       "os"
-       "os/exec"
-
-       check "gopkg.in/check.v1"
-)
-
-func (s *PamSuite) TestLoginLDAPViaPAM(c *check.C) {
-       cmd := exec.Command("bash", "login_pam_docker_test.sh")
-       cmd.Stdout = os.Stderr
-       cmd.Stderr = os.Stderr
-       err := cmd.Run()
-       c.Check(err, check.IsNil)
-}
index 5b0e4533e1844c543539ebab7567e8ca2973d82c..e6b967c9440b887cc6b2e68bd3ccb5e7a8fa78eb 100644 (file)
@@ -32,8 +32,8 @@ func (s *PamSuite) SetUpSuite(c *check.C) {
        c.Assert(err, check.IsNil)
        s.cluster, err = cfg.GetCluster("")
        c.Assert(err, check.IsNil)
-       s.cluster.Login.PAM = true
-       s.cluster.Login.PAMDefaultEmailDomain = "example.com"
+       s.cluster.Login.PAM.Enable = true
+       s.cluster.Login.PAM.DefaultEmailDomain = "example.com"
        s.railsSpy = arvadostest.NewProxy(c, s.cluster.Services.RailsAPI)
        s.ctrl = &pamLoginController{
                Cluster:    s.cluster,
@@ -79,6 +79,6 @@ func (s *PamSuite) TestLoginSuccess(c *check.C) {
        c.Check(resp.Scopes, check.DeepEquals, []string{"all"})
 
        authinfo := getCallbackAuthInfo(c, s.railsSpy)
-       c.Check(authinfo.Email, check.Equals, u+"@"+s.cluster.Login.PAMDefaultEmailDomain)
+       c.Check(authinfo.Email, check.Equals, u+"@"+s.cluster.Login.PAM.DefaultEmailDomain)
        c.Check(authinfo.AlternateEmails, check.DeepEquals, []string(nil))
 }
index 4e1dc73746a17e20a4e893b5aa877b2d681d0f78..ba57c20c357baeab68d92a5e41d52f8dc208606f 100644 (file)
@@ -311,19 +311,12 @@ rm ${zip}
                        }
                        defer func() {
                                cmd.Process.Signal(syscall.SIGTERM)
-                               logger.Infof("sent SIGTERM; waiting for postgres to shut down")
+                               logger.Info("sent SIGTERM; waiting for postgres to shut down")
                                cmd.Wait()
                        }()
-                       for deadline := time.Now().Add(10 * time.Second); ; {
-                               output, err2 := exec.Command("pg_isready").CombinedOutput()
-                               if err2 == nil {
-                                       break
-                               } else if time.Now().After(deadline) {
-                                       err = fmt.Errorf("timed out waiting for pg_isready (%q)", output)
-                                       return 1
-                               } else {
-                                       time.Sleep(time.Second)
-                               }
+                       err = waitPostgreSQLReady()
+                       if err != nil {
+                               return 1
                        }
                }
 
@@ -334,6 +327,51 @@ rm ${zip}
                        // might never have been run.
                }
 
+               var needcoll []string
+               // If the en_US.UTF-8 locale wasn't installed when
+               // postgresql initdb ran, it needs to be added
+               // explicitly before we can use it in our test suite.
+               for _, collname := range []string{"en_US", "en_US.UTF-8"} {
+                       cmd := exec.Command("sudo", "-u", "postgres", "psql", "-t", "-c", "SELECT 1 FROM pg_catalog.pg_collation WHERE collname='"+collname+"' AND collcollate IN ('en_US.UTF-8', 'en_US.utf8')")
+                       cmd.Dir = "/"
+                       out, err2 := cmd.CombinedOutput()
+                       if err != nil {
+                               err = fmt.Errorf("error while checking postgresql collations: %s", err2)
+                               return 1
+                       }
+                       if strings.Contains(string(out), "1") {
+                               logger.Infof("postgresql supports collation %s", collname)
+                       } else {
+                               needcoll = append(needcoll, collname)
+                       }
+               }
+               if len(needcoll) > 0 && os.Getpid() != 1 {
+                       // In order for the CREATE COLLATION statement
+                       // below to work, the locale must have existed
+                       // when PostgreSQL started up. If we're
+                       // running as init, we must have started
+                       // PostgreSQL ourselves after installing the
+                       // locales. Otherwise, it might need a
+                       // restart, so we attempt to restart it with
+                       // systemd.
+                       if err = runBash(`sudo systemctl restart postgresql`, stdout, stderr); err != nil {
+                               logger.Warn("`systemctl restart postgresql` failed; hoping postgresql does not need to be restarted")
+                       } else if err = waitPostgreSQLReady(); err != nil {
+                               return 1
+                       }
+               }
+               for _, collname := range needcoll {
+                       cmd := exec.Command("sudo", "-u", "postgres", "psql", "-c", "CREATE COLLATION \""+collname+"\" (LOCALE = \"en_US.UTF-8\")")
+                       cmd.Stdout = stdout
+                       cmd.Stderr = stderr
+                       cmd.Dir = "/"
+                       err = cmd.Run()
+                       if err != nil {
+                               err = fmt.Errorf("error adding postgresql collation %s: %s", collname, err)
+                               return 1
+                       }
+               }
+
                withstuff := "WITH LOGIN SUPERUSER ENCRYPTED PASSWORD " + pq.QuoteLiteral(devtestDatabasePassword)
                cmd := exec.Command("sudo", "-u", "postgres", "psql", "-c", "ALTER ROLE arvados "+withstuff)
                cmd.Dir = "/"
@@ -408,6 +446,19 @@ func identifyOS() (osversion, error) {
        return osv, nil
 }
 
+func waitPostgreSQLReady() error {
+       for deadline := time.Now().Add(10 * time.Second); ; {
+               output, err := exec.Command("pg_isready").CombinedOutput()
+               if err == nil {
+                       return nil
+               } else if time.Now().After(deadline) {
+                       return fmt.Errorf("timed out waiting for pg_isready (%q)", output)
+               } else {
+                       time.Sleep(time.Second)
+               }
+       }
+}
+
 func runBash(script string, stdout, stderr io.Writer) error {
        cmd := exec.Command("bash", "-")
        cmd.Stdin = bytes.NewBufferString("set -ex -o pipefail\n" + script)
index 5c47532db9ac9efa12ff9cc10b4e17b9d8ec9ae1..66176b940b0eff5497cbbf995f78ddb65cf0ae5e 100644 (file)
@@ -6,8 +6,11 @@ case "$TARGET" in
     debian8)
         fpm_depends+=(libgnutls-deb0-28 libcurl3-gnutls)
         ;;
+    debian9 | ubuntu1604)
+        fpm_depends+=(libcurl3-gnutls)
+        ;;
     debian* | ubuntu*)
-        fpm_depends+=(libcurl3-gnutls libpython2.7)
+        fpm_depends+=(libcurl3-gnutls python3-distutils)
         ;;
 esac
 
index 880a91ee6965fdd0d4adc2aef3d397a156caf821..1efc87ea72ac6f67496e0b4df931905092f2c6fa 100644 (file)
@@ -135,16 +135,39 @@ type Cluster struct {
                Repositories string
        }
        Login struct {
-               GoogleClientID                string
-               GoogleClientSecret            string
-               GoogleAlternateEmailAddresses bool
-               PAM                           bool
-               PAMService                    string
-               PAMDefaultEmailDomain         string
-               ProviderAppID                 string
-               ProviderAppSecret             string
-               LoginCluster                  string
-               RemoteTokenRefresh            Duration
+               LDAP struct {
+                       Enable             bool
+                       URL                URL
+                       StartTLS           bool
+                       InsecureTLS        bool
+                       StripDomain        string
+                       AppendDomain       string
+                       SearchAttribute    string
+                       SearchBindUser     string
+                       SearchBindPassword string
+                       SearchBase         string
+                       SearchFilters      string
+                       EmailAttribute     string
+                       UsernameAttribute  string
+               }
+               Google struct {
+                       Enable                  bool
+                       ClientID                string
+                       ClientSecret            string
+                       AlternateEmailAddresses bool
+               }
+               PAM struct {
+                       Enable             bool
+                       Service            string
+                       DefaultEmailDomain string
+               }
+               SSO struct {
+                       Enable            bool
+                       ProviderAppID     string
+                       ProviderAppSecret string
+               }
+               LoginCluster       string
+               RemoteTokenRefresh Duration
        }
        Mail struct {
                MailchimpAPIKey                string
index 779ac4bfc0614f853da77e77068eb8ed539f8865..fe32547fcbda14eaa712924a0eb68623310dff96 100644 (file)
@@ -757,8 +757,10 @@ def setup_config():
                     "RailsSessionSecretToken": "e24205c490ac07e028fd5f8a692dcb398bcd654eff1aef5f9fe6891994b18483",
                 },
                 "Login": {
-                    "ProviderAppID": "arvados-server",
-                    "ProviderAppSecret": "608dbf356a327e2d0d4932b60161e212c2d8d8f5e25690d7b622f850a990cd33",
+                    "SSO": {
+                        "ProviderAppID": "arvados-server",
+                        "ProviderAppSecret": "608dbf356a327e2d0d4932b60161e212c2d8d8f5e25690d7b622f850a990cd33",
+                    },
                 },
                 "SystemLogs": {
                     "LogLevel": ('info' if os.environ.get('ARVADOS_DEBUG', '') in ['','0'] else 'debug'),
index d9ab5556ffc9ac7826abda00bc18e3d4b700269c..867b9a6e6abfdf0ae050a668f4340d1664608586 100644 (file)
@@ -54,7 +54,11 @@ class Arvados::V1::UsersController < ApplicationController
       @object = current_user
     end
     if not @object.is_active
-      if not (current_user.is_admin or @object.is_invited)
+      if @object.uuid[0..4] == Rails.configuration.Login.LoginCluster &&
+         @object.uuid[0..4] != Rails.configuration.ClusterID
+        logger.warn "Local user #{@object.uuid} called users#activate but only LoginCluster can do that"
+        raise ArgumentError.new "cannot activate user #{@object.uuid} here, only the #{@object.uuid[0..4]} cluster can do that"
+      elsif not (current_user.is_admin or @object.is_invited)
         logger.warn "User #{@object.uuid} called users.activate " +
           "but is not invited"
         raise ArgumentError.new "Cannot activate without being invited."
index dd447ca51a895fa2297d6860002a52ff7f360037..c3641b64e84f04217145edacab05ac8d84f259a7 100644 (file)
@@ -238,8 +238,14 @@ class User < ArvadosModel
   end
 
   def must_unsetup_to_deactivate
-    if self.is_active_changed? &&
-       self.is_active_was == true &&
+    if !self.new_record? &&
+       self.uuid[0..4] == Rails.configuration.Login.LoginCluster &&
+       self.uuid[0..4] != Rails.configuration.ClusterID
+      # OK to update our local record to whatever the LoginCluster
+      # reports, because self-activate is not allowed.
+      return
+    elsif self.is_active_changed? &&
+       self.is_active_was &&
        !self.is_active
 
       group = Group.where(name: 'All users').select do |g|
index 502e3e787d1e1324217983f73cd841bce0e1ea1a..f63f8af0335884c606ba2c52117d939657b4ff1e 100644 (file)
@@ -37,8 +37,8 @@ EOS
   # Real values will be copied from globals by omniauth_init.rb. For
   # now, assign some strings so the generic *.yml config loader
   # doesn't overwrite them or complain that they're missing.
-  Rails.configuration.Login["ProviderAppID"] = 'xxx'
-  Rails.configuration.Login["ProviderAppSecret"] = 'xxx'
+  Rails.configuration.Login["SSO"]["ProviderAppID"] = 'xxx'
+  Rails.configuration.Login["SSO"]["ProviderAppSecret"] = 'xxx'
   Rails.configuration.Services["SSO"]["ExternalURL"] = '//xxx'
   WARNED_OMNIAUTH_CONFIG = true
 end
@@ -106,8 +106,8 @@ arvcfg.declare_config "Users.EmailSubjectPrefix", String, :email_subject_prefix
 arvcfg.declare_config "Users.UserNotifierEmailFrom", String, :user_notifier_email_from
 arvcfg.declare_config "Users.NewUserNotificationRecipients", Hash, :new_user_notification_recipients, ->(cfg, k, v) { arrayToHash cfg, "Users.NewUserNotificationRecipients", v }
 arvcfg.declare_config "Users.NewInactiveUserNotificationRecipients", Hash, :new_inactive_user_notification_recipients, method(:arrayToHash)
-arvcfg.declare_config "Login.ProviderAppSecret", String, :sso_app_secret
-arvcfg.declare_config "Login.ProviderAppID", String, :sso_app_id
+arvcfg.declare_config "Login.SSO.ProviderAppSecret", String, :sso_app_secret
+arvcfg.declare_config "Login.SSO.ProviderAppID", String, :sso_app_id
 arvcfg.declare_config "Login.LoginCluster", String
 arvcfg.declare_config "Login.RemoteTokenRefresh", ActiveSupport::Duration
 arvcfg.declare_config "TLS.Insecure", Boolean, :sso_insecure
@@ -190,6 +190,7 @@ dbcfg.declare_config "PostgreSQL.Connection.password", String, :password
 dbcfg.declare_config "PostgreSQL.Connection.dbname", String, :database
 dbcfg.declare_config "PostgreSQL.Connection.template", String, :template
 dbcfg.declare_config "PostgreSQL.Connection.encoding", String, :encoding
+dbcfg.declare_config "PostgreSQL.Connection.collation", String, :collation
 
 application_config = {}
 %w(application.default application).each do |cfgfile|
@@ -257,6 +258,8 @@ if ::Rails.env.to_s == "test"
   # Use template0 when creating a new database. Avoids
   # character-encoding/collation problems.
   $arvados_config["PostgreSQL"]["Connection"]["template"] = "template0"
+  # Some test cases depend on en_US.UTF-8 collation.
+  $arvados_config["PostgreSQL"]["Connection"]["collation"] = "en_US.UTF-8"
 end
 
 if $arvados_config["PostgreSQL"]["Connection"]["password"].empty?
@@ -279,6 +282,7 @@ ENV["DATABASE_URL"] = "postgresql://#{$arvados_config["PostgreSQL"]["Connection"
                       "#{dbhost}/#{$arvados_config["PostgreSQL"]["Connection"]["dbname"]}?"+
                       "template=#{$arvados_config["PostgreSQL"]["Connection"]["template"]}&"+
                       "encoding=#{$arvados_config["PostgreSQL"]["Connection"]["client_encoding"]}&"+
+                      "collation=#{$arvados_config["PostgreSQL"]["Connection"]["collation"]}&"+
                       "pool=#{$arvados_config["PostgreSQL"]["ConnectionPool"]}"
 
 Server::Application.configure do
index 35a318b94fe1f5bf4bc822f06608c8b031854a1e..a1b2356bd56242389cff2dac821ad7d68f103177 100644 (file)
@@ -9,14 +9,14 @@
 
 if defined? CUSTOM_PROVIDER_URL
   Rails.logger.warn "Copying omniauth from globals in legacy config file."
-  Rails.configuration.Login["ProviderAppID"] = APP_ID
-  Rails.configuration.Login["ProviderAppSecret"] = APP_SECRET
+  Rails.configuration.Login["SSO"]["ProviderAppID"] = APP_ID
+  Rails.configuration.Login["SSO"]["ProviderAppSecret"] = APP_SECRET
   Rails.configuration.Services["SSO"]["ExternalURL"] = CUSTOM_PROVIDER_URL.sub(/\/$/, "") + "/"
 else
   Rails.application.config.middleware.use OmniAuth::Builder do
     provider(:josh_id,
-             Rails.configuration.Login["ProviderAppID"],
-             Rails.configuration.Login["ProviderAppSecret"],
+             Rails.configuration.Login["SSO"]["ProviderAppID"],
+             Rails.configuration.Login["SSO"]["ProviderAppSecret"],
              Rails.configuration.Services["SSO"]["ExternalURL"])
   end
   OmniAuth.config.on_failure = StaticController.action(:login_failure)
index ceccd11c92172a0f018ec87f25a95bfdada2bf33..5026e2d32558e085886ba119cf0b664bfbc58473 100644 (file)
@@ -172,10 +172,10 @@ func (v *UnixVolume) Touch(loc string) error {
                return e
        }
        defer v.unlockfile(f)
-       ts := syscall.NsecToTimespec(time.Now().UnixNano())
+       ts := time.Now()
        v.os.stats.TickOps("utimes")
        v.os.stats.Tick(&v.os.stats.UtimesOps)
-       err = syscall.UtimesNano(p, []syscall.Timespec{ts, ts})
+       err = os.Chtimes(p, ts, ts)
        v.os.stats.TickErr(err)
        return err
 }
@@ -298,6 +298,19 @@ func (v *UnixVolume) WriteBlock(ctx context.Context, loc string, rdr io.Reader)
                v.os.Remove(tmpfile.Name())
                return err
        }
+       // ext4 uses a low-precision clock and effectively backdates
+       // files by up to 10 ms, sometimes across a 1-second boundary,
+       // which produces confusing results in logs and tests.  We
+       // avoid this by setting the output file's timestamps
+       // explicitly, using a higher resolution clock.
+       ts := time.Now()
+       v.os.stats.TickOps("utimes")
+       v.os.stats.Tick(&v.os.stats.UtimesOps)
+       if err = os.Chtimes(tmpfile.Name(), ts, ts); err != nil {
+               err = fmt.Errorf("error setting timestamps on %s: %s", tmpfile.Name(), err)
+               v.os.Remove(tmpfile.Name())
+               return err
+       }
        if err := v.os.Rename(tmpfile.Name(), bpath); err != nil {
                err = fmt.Errorf("error renaming %s to %s: %s", tmpfile.Name(), bpath, err)
                v.os.Remove(tmpfile.Name())
index 7777363b9d13815ab3036ae916a2c0f6989eb95f..5a3a536944daa5b8012bc0b2afbf8b6932862364 100644 (file)
@@ -405,13 +405,13 @@ func (s *UnixVolumeSuite) TestStats(c *check.C) {
        c.Check(stats(), check.Matches, `.*"OutBytes":3,.*`)
        c.Check(stats(), check.Matches, `.*"CreateOps":1,.*`)
        c.Check(stats(), check.Matches, `.*"OpenOps":0,.*`)
-       c.Check(stats(), check.Matches, `.*"UtimesOps":0,.*`)
+       c.Check(stats(), check.Matches, `.*"UtimesOps":1,.*`)
 
        err = vol.Touch(loc)
        c.Check(err, check.IsNil)
        c.Check(stats(), check.Matches, `.*"FlockOps":1,.*`)
        c.Check(stats(), check.Matches, `.*"OpenOps":1,.*`)
-       c.Check(stats(), check.Matches, `.*"UtimesOps":1,.*`)
+       c.Check(stats(), check.Matches, `.*"UtimesOps":2,.*`)
 
        _, err = vol.Get(context.Background(), loc, make([]byte, 3))
        c.Check(err, check.IsNil)
index ed4795d1cc8676cfdd93c052cd44cbffae08de98..dbcbc913cecdd5946461ee838376a32aec44f83f 100755 (executable)
@@ -139,8 +139,9 @@ Clusters:
       DefaultReplication: 1
       TrustAllContent: true
     Login:
-      ProviderAppSecret: $sso_app_secret
-      ProviderAppID: arvados-server
+      SSO:
+        ProviderAppSecret: $sso_app_secret
+        ProviderAppID: arvados-server
     Users:
       NewUsersAreActive: true
       AutoAdminFirstUser: true