17296: Merge branch 'master'
authorTom Clegg <tom@curii.com>
Tue, 25 May 2021 21:05:20 +0000 (17:05 -0400)
committerTom Clegg <tom@curii.com>
Tue, 25 May 2021 21:05:20 +0000 (17:05 -0400)
Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tom@curii.com>

41 files changed:
apps/workbench/Gemfile.lock
apps/workbench/app/models/arvados_resource_list.rb
apps/workbench/app/views/users/welcome.html.erb
doc/_config.yml
doc/api/methods/users.html.textile.liquid
doc/api/requests.html.textile.liquid
doc/api/tokens.html.textile.liquid
doc/install/container-shell-access.html.textile.liquid [new file with mode: 0644]
doc/install/install-manual-prerequisites.html.textile.liquid
doc/install/setup-login.html.textile.liquid
doc/user/cwl/costanalyzer.html.textile.liquid [new file with mode: 0644]
doc/user/debugging/container-shell-access.html.textile.liquid [new file with mode: 0644]
lib/config/config.default.yml
lib/config/export.go
lib/config/generated_config.go
lib/controller/auth_test.go
lib/controller/federation/list.go
lib/controller/integration_test.go
lib/controller/localdb/login.go
lib/controller/localdb/login_oidc.go
lib/controller/localdb/login_oidc_test.go
lib/costanalyzer/costanalyzer.go
lib/crunchrun/copier.go
lib/crunchrun/copier_test.go
sdk/go/arvados/config.go
sdk/go/arvadostest/oidc_provider.go
services/api/Gemfile.lock
services/keep-web/cache.go
services/keep-web/handler_test.go
tools/salt-install/config_examples/multi_host/aws/certs/README.md
tools/salt-install/config_examples/multi_host/aws/pillars/arvados.sls
tools/salt-install/config_examples/multi_host/aws/pillars/letsencrypt.sls
tools/salt-install/config_examples/multi_host/aws/pillars/letsencrypt_keepweb_configuration.sls
tools/salt-install/config_examples/multi_host/aws/pillars/nginx_controller_configuration.sls
tools/salt-install/config_examples/multi_host/aws/pillars/nginx_keepproxy_configuration.sls
tools/salt-install/config_examples/multi_host/aws/pillars/nginx_keepweb_configuration.sls
tools/salt-install/config_examples/multi_host/aws/pillars/nginx_webshell_configuration.sls
tools/salt-install/config_examples/multi_host/aws/pillars/nginx_websocket_configuration.sls
tools/salt-install/config_examples/multi_host/aws/pillars/nginx_workbench2_configuration.sls
tools/salt-install/config_examples/multi_host/aws/pillars/nginx_workbench_configuration.sls
tools/salt-install/provision.sh

index e4ef96b194f84306b778870a36562c0b7d7b7703..0f29c543b4fbaa9fb2fbc1ef5d6a0f8adaec2eda 100644 (file)
@@ -189,7 +189,7 @@ GEM
     mimemagic (0.3.8)
       nokogiri (~> 1)
     mini_mime (1.0.2)
-    mini_portile2 (2.5.0)
+    mini_portile2 (2.5.1)
     minitest (5.10.3)
     mocha (1.8.0)
       metaclass (~> 0.0.1)
@@ -206,7 +206,7 @@ GEM
     net-ssh-gateway (2.0.0)
       net-ssh (>= 4.0.0)
     nio4r (2.5.7)
-    nokogiri (1.11.2)
+    nokogiri (1.11.5)
       mini_portile2 (~> 2.5.0)
       racc (~> 1.4)
     npm-rails (0.2.1)
index 99502bd56ed04951695e8bcb15704b64ea4b46e5..75a9429a43739f7f3c024f496d316b1d4e69cf86 100644 (file)
@@ -223,6 +223,7 @@ class ArvadosResourceList
     api_params[:filters] = @filters if @filters
     api_params[:distinct] = @distinct if @distinct
     api_params[:include_trash] = @include_trash if @include_trash
+    api_params[:cluster_id] = Rails.configuration.ClusterID
     if @fetch_multiple_pages
       # Default limit to (effectively) api server's MAX_LIMIT
       api_params[:limit] = 2**(0.size*8 - 1) - 1
index 0b98909e67d81f036558153e2fb662ff70af1f30..92fd6dad4615c1e618663c08e237e786c3f659fd 100644 (file)
@@ -47,18 +47,9 @@ SPDX-License-Identifier: AGPL-3.0 %>
       <%= raw(Rails.configuration.Workbench.WelcomePageHTML) %>
 
       <% case %>
-      <% when Rails.configuration.Login.Google.Enable %>
-      <% when Rails.configuration.Login.OpenIDConnect.Enable %>
-      <% when Rails.configuration.Login.SSO.Enable %>
-        <div class="pull-right">
-          <%= link_to arvados_api_client.arvados_login_url(return_to: request.url), class: "btn btn-primary" do %>
-          Log in to <%= Rails.configuration.Workbench.SiteName %>
-          <i class="fa fa-fw fa-arrow-circle-right"></i>
-          <% end %>
-        </div>
-      <% when Rails.configuration.Login.PAM.Enable %>
-      <% when Rails.configuration.Login.LDAP.Enable %>
-      <% when Rails.configuration.Login.Test.Enable %>
+      <% when Rails.configuration.Login.PAM.Enable,
+              Rails.configuration.Login.LDAP.Enable,
+              Rails.configuration.Login.Test.Enable %>
         <form id="login-form-tag" onsubmit="controller_password_authenticate(event)">
           <p>username <input type="text" class="form-control" name="login-username"
                             value="" id="login-username" style="width: 50%"
@@ -70,6 +61,13 @@ SPDX-License-Identifier: AGPL-3.0 %>
         <span style="color: red"><p id="login-authenticate-error"></p></span>
         <button type="submit" class="btn btn-primary">Log in</button>
         </form>
+      <% else %>
+        <div class="pull-right">
+          <%= link_to arvados_api_client.arvados_login_url(return_to: request.url), class: "btn btn-primary" do %>
+          Log in to <%= Rails.configuration.Workbench.SiteName %>
+          <i class="fa fa-fw fa-arrow-circle-right"></i>
+          <% end %>
+        </div>
       <% end %>
 
     </div>
index d6f91b36c1311830077eec30e476704d324f0d67..55987c062fad7666e4541477b60584788fc7027f 100644 (file)
@@ -58,6 +58,8 @@ navbar:
       - user/cwl/federated-workflows.html.textile.liquid
       - user/cwl/cwl-versions.html.textile.liquid
       - user/cwl/crunchstat-summary.html.textile.liquid
+      - user/cwl/costanalyzer.html.textile.liquid
+      - user/debugging/container-shell-access.html.textile.liquid
     - Working with git repositories:
       - user/tutorials/add-new-repository.html.textile.liquid
       - user/tutorials/git-arvados-guide.html.textile.liquid
@@ -251,6 +253,8 @@ navbar:
       - install/crunch2-slurm/configure-slurm.html.textile.liquid
       - install/crunch2-slurm/install-compute-node.html.textile.liquid
       - install/crunch2-slurm/install-test.html.textile.liquid
+    - Additional configuration:
+      - install/container-shell-access.html.textile.liquid
     - External dependencies:
       - install/install-postgresql.html.textile.liquid
       - install/ruby.html.textile.liquid
index 6db8d963e744b9a85459501ccf69bcf892321a11..a4d4aade9b581eaa6b2de3c7e30a5d7a5ac57eff 100644 (file)
@@ -172,3 +172,18 @@ table(table table-bordered table-condensed).
 |old_user_uuid|uuid|The uuid of the "old" account|query||
 |new_owner_uuid|uuid|The uuid of a project to which objects owned by the "old" user will be reassigned.|query||
 |redirect_to_new_user|boolean|If true, also redirect login and reassign authorization credentials from "old" user to the "new" user|query||
+
+h3. authenticate
+
+Create a new API token based on username/password credentials.  Returns an "API client authorization":api_client_authorizations.html object containing the API token, or an "error object.":../requests.html#errors
+
+Valid credentials are determined by the choice of "configured login backend.":{{site.baseurl}}/install/setup-login.html
+
+Note: this endpoint cannot be used with login backends that use web-based third party authentication, such as Google or OpenID Connect.
+
+Arguments:
+
+table(table table-bordered table-condensed).
+|_. Argument |_. Type |_. Description |_. Location |_. Example |
+{background:#ccffcc}.|username|string|The username.|body||
+{background:#ccffcc}.|password|string|The password.|body||
index 84cae49a01b36c8f806c085ce47d60baa737ad1b..fc5957af5ff0c273681ed6fc95ec6ff603680d53 100644 (file)
@@ -35,13 +35,19 @@ Every request must include an API token.  This identifies the user making the re
 API requests must provide the API token using the @Authorization@ header in the following format:
 
 <pre>
-$ curl -v -H "Authorization: OAuth2 xxxxapitokenxxxx" https://192.168.5.2:8000/arvados/v1/collections
+$ curl -v -H "Authorization: Bearer xxxxapitokenxxxx" https://192.168.5.2:8000/arvados/v1/collections
 > GET /arvados/v1/collections HTTP/1.1
 > ...
-> Authorization: OAuth2 xxxxapitokenxxxx
+> Authorization: Bearer xxxxapitokenxxxx
 > ...
 </pre>
 
+On a cluster configured to use an OpenID Connect provider (other than Google) as a login backend, Arvados can be configured to accept an OpenID Connect access token in place of an Arvados API token. OIDC access tokens are also accepted by a cluster that delegates login to another cluster (LoginCluster) which in turn has this feature configured. See @Login.OpenIDConnect.AcceptAccessTokenScope@ in the "default config.yml file":{{site.baseurl}}/admin/config.html for details.
+
+<pre>
+$ curl -v -H "Authorization: Bearer xxxx-openid-connect-access-token-xxxx" https://192.168.5.2:8000/arvados/v1/collections
+</pre>
+
 h3. Parameters
 
 Request parameters may be provided in one of two ways.  They may be provided in the "query" section of request URI, or they may be provided in the body of the request with application/x-www-form-urlencoded encoding.  If parameters are provided in both places, their values will be merged.  Parameter names must be unique.  If a parameter appears multiple times, the behavior is undefined.
@@ -52,7 +58,7 @@ h3. Result
 
 Results are returned JSON-encoded in the response body.
 
-h3. Errors
+h3(#errors). Errors
 
 If a request cannot be fulfilled, the API will return 4xx or 5xx HTTP status code.  Be aware that the API server may return a 404 (Not Found) status for resources that exist but for which the client does not have read access.  The API will also return an error record:
 
@@ -66,12 +72,12 @@ h2. Examples
 h3. Create a new record
 
 <pre>
-$ curl -v -X POST --data-urlencode 'collection={"name":"empty collection"}' -H "Authorization: OAuth2 oz0os4nyudswvglxhdlnrgnuelxptmj7qu7dpwvyz3g9ocqtr" https://192.168.5.2:8000/arvados/v1/collections | jq .
+$ curl -v -X POST --data-urlencode 'collection={"name":"empty collection"}' -H "Authorization: Bearer oz0os4nyudswvglxhdlnrgnuelxptmj7qu7dpwvyz3g9ocqtr" https://192.168.5.2:8000/arvados/v1/collections | jq .
 > POST /arvados/v1/collections HTTP/1.1
 > User-Agent: curl/7.38.0
 > Host: 192.168.5.2:8000
 > Accept: */*
-> Authorization: OAuth2 oz0os4nyudswvglxhdlnrgnuelxptmj7qu7dpwvyz3g9ocqtr
+> Authorization: Bearer oz0os4nyudswvglxhdlnrgnuelxptmj7qu7dpwvyz3g9ocqtr
 > Content-Length: 54
 > Content-Type: application/x-www-form-urlencoded
 >
@@ -120,12 +126,12 @@ $ curl -v -X POST --data-urlencode 'collection={"name":"empty collection"}' -H "
 h3. Delete a record
 
 <pre>
-$ curl -X DELETE -v -H "Authorization: OAuth2 oz0os4nyudswvglxhdlnrgnuelxptmj7qu7dpwvyz3g9ocqtr" https://192.168.5.2:8000/arvados/v1/collections/962eh-4zz18-m1ma0mxxfg3mbcc | jq .
+$ curl -X DELETE -v -H "Authorization: Bearer oz0os4nyudswvglxhdlnrgnuelxptmj7qu7dpwvyz3g9ocqtr" https://192.168.5.2:8000/arvados/v1/collections/962eh-4zz18-m1ma0mxxfg3mbcc | jq .
 > DELETE /arvados/v1/collections/962eh-4zz18-m1ma0mxxfg3mbcc HTTP/1.1
 > User-Agent: curl/7.38.0
 > Host: 192.168.5.2:8000
 > Accept: */*
-> Authorization: OAuth2 oz0os4nyudswvglxhdlnrgnuelxptmj7qu7dpwvyz3g9ocqtr
+> Authorization: Bearer oz0os4nyudswvglxhdlnrgnuelxptmj7qu7dpwvyz3g9ocqtr
 >
 < HTTP/1.1 200 OK
 < Content-Type: application/json; charset=utf-8
@@ -171,12 +177,12 @@ $ curl -X DELETE -v -H "Authorization: OAuth2 oz0os4nyudswvglxhdlnrgnuelxptmj7qu
 h3. Get a specific record
 
 <pre>
-$ curl -v -H "Authorization: OAuth2 oz0os4nyudswvglxhdlnrgnuelxptmj7qu7dpwvyz3g9ocqtr" https://192.168.5.2:8000/arvados/v1/collections/962eh-4zz18-xi32mpz2621o8km | jq .
+$ curl -v -H "Authorization: Bearer oz0os4nyudswvglxhdlnrgnuelxptmj7qu7dpwvyz3g9ocqtr" https://192.168.5.2:8000/arvados/v1/collections/962eh-4zz18-xi32mpz2621o8km | jq .
 > GET /arvados/v1/collections/962eh-4zz18-xi32mpz2621o8km HTTP/1.1
 > User-Agent: curl/7.38.0
 > Host: 192.168.5.2:8000
 > Accept: */*
-> Authorization: OAuth2 oz0os4nyudswvglxhdlnrgnuelxptmj7qu7dpwvyz3g9ocqtr
+> Authorization: Bearer oz0os4nyudswvglxhdlnrgnuelxptmj7qu7dpwvyz3g9ocqtr
 >
 < HTTP/1.1 200 OK
 < Content-Type: application/json; charset=utf-8
@@ -223,12 +229,12 @@ h3. List records and filter by date
 (Note, return result is truncated).
 
 <pre>
-$ curl -v -G --data-urlencode 'filters=[["created_at",">","2016-11-08T21:38:24.124834000Z"]]' -H "Authorization: OAuth2 oz0os4nyudswvglxhdlnrgnuelxptmj7qu7dpwvyz3g9ocqtr" https://192.168.5.2:8000/arvados/v1/collections | jq .
+$ curl -v -G --data-urlencode 'filters=[["created_at",">","2016-11-08T21:38:24.124834000Z"]]' -H "Authorization: Bearer oz0os4nyudswvglxhdlnrgnuelxptmj7qu7dpwvyz3g9ocqtr" https://192.168.5.2:8000/arvados/v1/collections | jq .
 > GET /arvados/v1/collections?filters=%5B%5B%22uuid%22%2C%20%22%3D%22%2C%20%22962eh-4zz18-xi32mpz2621o8km%22%5D%5D HTTP/1.1
 > User-Agent: curl/7.38.0
 > Host: 192.168.5.2:8000
 > Accept: */*
-> Authorization: OAuth2 oz0os4nyudswvglxhdlnrgnuelxptmj7qu7dpwvyz3g9ocqtr
+> Authorization: Bearer oz0os4nyudswvglxhdlnrgnuelxptmj7qu7dpwvyz3g9ocqtr
 >
 < HTTP/1.1 200 OK
 < Content-Type: application/json; charset=utf-8
@@ -302,12 +308,12 @@ $ curl -v -G --data-urlencode 'filters=[["created_at",">","2016-11-08T21:38:24.1
 h3. Update a field
 
 <pre>
-$ curl -v -X PUT --data-urlencode 'collection={"name":"rna.SRR948778.bam"}' -H "Authorization: OAuth2 oz0os4nyudswvglxhdlnrgnuelxptmj7qu7dpwvyz3g9ocqtr" https://192.168.5.2:8000/arvados/v1/collections/962eh-4zz18-xi32mpz2621o8km | jq .
+$ curl -v -X PUT --data-urlencode 'collection={"name":"rna.SRR948778.bam"}' -H "Authorization: Bearer oz0os4nyudswvglxhdlnrgnuelxptmj7qu7dpwvyz3g9ocqtr" https://192.168.5.2:8000/arvados/v1/collections/962eh-4zz18-xi32mpz2621o8km | jq .
 > PUT /arvados/v1/collections/962eh-4zz18-xi32mpz2621o8km HTTP/1.1
 > User-Agent: curl/7.38.0
 > Host: 192.168.5.2:8000
 > Accept: */*
-> Authorization: OAuth2 oz0os4nyudswvglxhdlnrgnuelxptmj7qu7dpwvyz3g9ocqtr
+> Authorization: Bearer oz0os4nyudswvglxhdlnrgnuelxptmj7qu7dpwvyz3g9ocqtr
 > Content-Length: 53
 > Content-Type: application/x-www-form-urlencoded
 >
index 9d8f456509b12d730d2d22bdcae6a8b785f74eb6..0935f9ba1d2a3bf7eb5c5bb7db4eb20b528ac3ed 100644 (file)
@@ -11,19 +11,39 @@ SPDX-License-Identifier: CC-BY-SA-3.0
 
 All requests to the API server must have an API token.  API tokens can be issued by going though the login flow, or created via the API.  At this time, only browser based applications can perform login from email/password.  Command line applications and services must use an API token provided via the @ARVADOS_API_TOKEN@ environment variable or configuration file.
 
-h2. Browser login
+h2. Login
 
-Browser based applications can perform log in via the following highlevel flow:
+Browser based applications can log in using one of the two possible flows:
 
-# The web application presents a "login" link to @/login@ on the API server with a @return_to@ parameter provided in the query portion of the URL.  For example @https://{{ site.arvados_api_host }}/login?return_to=XXX@ , where  @return_to=XXX@ is the URL of the login page for the web application.
-# The "login" link takes the browser to the login page (this may involve several redirects)
-# The user logs in.  API server authenticates the user and issues a new API token.
-# The browser is redirected to the login page URL provided in @return_to=XXX@ with the addition of @?api_token=xxxxapitokenxxxx@.
-# The web application gets the login request with the included authorization token.
+h3. Authenticate via a third party
 
-!{{site.baseurl}}/images/Session_Establishment.svg!
+# The web application instructs the user to click on a link to the @/login@ endpoint on the API server.  This link should include the @return_to@ parameter in the query portion of the URL.  For example @https://{{ site.arvados_api_host }}/login?return_to=XXX@ , where  @return_to=XXX@ is a page in the web application.
+# The @/login@ endpoint redirects the user to the configured third party authentication provider (e.g. Google or other OpenID Connect provider).
+# The user logs in to the third party provider, then they are redirected back to the API server.
+# The API server authenticates the user, issues a new API token, and redirects the browser to the URL provided in @return_to=XXX@ with the addition of @?api_token=xxxxapitokenxxxx@.
+# The web application gets the authorization token from the query and uses it to access the API server on the user's behalf.
+
+h3. Direct username/password authentication
+
+# The web application presents username and password fields.
+# When the submit button is pressed, using Javascript, the browser sends a POST request to @/arvados/v1/users/authenticate@
+** The request payload type is @application/javascript@
+** The request body is a JSON object with @username@ and @password@ fields.
+# The API server receives the username and password, authenticates them with the upstream provider (such as LDAP or PAM), and responds with the @api_client_authorization@ object for the new API token.
+# The web application receives the authorization token in the response and uses it to access the API server on the user's behalf.
+
+h3. Using an OpenID Connect access token
 
-The "browser authentication process is documented in detail on the Arvados wiki.":https://dev.arvados.org/projects/arvados/wiki/Workbench_authentication_process
+A cluster that uses OpenID Connect as a login provider can be configured to accept OIDC access tokens as well as Arvados API tokens (this is disabled by default; see @Login.OpenIDConnect.AcceptAccessToken@ in the "default config.yml file":{{site.baseurl}}/admin/config.html).
+# The client obtains an access token from the OpenID Connect provider via some method outside of Arvados.
+# The client presents the access token with an Arvados API request (e.g., request header @Authorization: Bearer xxxxaccesstokenxxxx@).
+# Depending on configuration, the API server decodes the access token (which must be a signed JWT) and confirms that it includes the required scope (see @Login.OpenIDConnect.AcceptAccessTokenScope@ in the "default config.yml file":{{site.baseurl}}/admin/config.html).
+# The API server uses the provider's UserInfo endpoint to validate the presented token.
+# If the token is valid, it is cached in the Arvados database and accepted in subsequent API calls for the next 10 minutes.
+
+h3. Diagram
+
+!{{site.baseurl}}/images/Session_Establishment.svg!
 
 h2. User activation
 
diff --git a/doc/install/container-shell-access.html.textile.liquid b/doc/install/container-shell-access.html.textile.liquid
new file mode 100644 (file)
index 0000000..e60382c
--- /dev/null
@@ -0,0 +1,44 @@
+---
+layout: default
+navsection: installguide
+title: Configure container shell access
+...
+{% comment %}
+Copyright (C) The Arvados Authors. All rights reserved.
+
+SPDX-License-Identifier: CC-BY-SA-3.0
+{% endcomment %}
+
+Arvados can be configured to permit shell access to running containers. This can be handy for debugging, but it could affect reproducability of workflows. This feature can be enabled for admin users, or for all users. By default, it is entirely disabled.
+
+The relevant configuration section is
+
+<notextile>
+<pre><code>    Containers:
+      ShellAccess:
+        # An admin user can use "arvados-client shell" to start an
+        # interactive shell (with any user ID) in any running
+        # container.
+        Admin: false
+
+        # Any user can use "arvados-client shell" to start an
+        # interactive shell (with any user ID) in any running
+        # container that they started, provided it isn't also
+        # associated with a different user's container request.
+        #
+        # Interactive sessions make it easy to alter the container's
+        # runtime environment in ways that aren't recorded or
+        # reproducible. Consider the implications for automatic
+        # container reuse before enabling and using this feature. In
+        # particular, note that starting an interactive session does
+        # not disqualify a container from being reused by a different
+        # user/workflow in the future.
+        User: false
+</code></pre>
+</notextile>
+
+To enable the feature a firewall change may also be required. This feature requires the opening of tcp connections from @arvados-controller@ to the range specified in the @net.ipv4.ip_local_port_range@ sysctl on compute nodes. If that range is unknown or hard to determine, it will be sufficient to allow tcp connections from @arvados-controller@ to port 1024-65535 on compute nodes, while allowing traffic that is part of existing tcp connections.
+
+After changing the configuration, @arvados-controller@ must be restarted for the change to take effect. When enabling, shell access will be enabled for any running containers. When disabling, access is removed immediately for any running containers, as well as any containers started subsequently. Restarting @arvados-controller@ will kill any active connections.
+
+Usage instructions for this feature are available in the "User guide":{{site.baseurl}}/user/debugging/container-shell-access.html.
index 364e8cd2bb2267e119961042e05a38f9eebb9b3f..73b54c462e91776ae76150e233863f97e34f8d6d 100644 (file)
@@ -30,9 +30,10 @@ table(table table-bordered table-condensed).
 |_. Distribution|_. State|_. Last supported version|
 |CentOS 7|Supported|Latest|
 |Debian 10 ("buster")|Supported|Latest|
+|Ubuntu 20.04 ("focal")|Supported|Latest|
 |Ubuntu 18.04 ("bionic")|Supported|Latest|
-|Ubuntu 16.04 ("xenial")|Supported|Latest|
-|Debian 9 ("stretch")|EOL|Latest 2.1.X release|
+|Ubuntu 16.04 ("xenial")|EOL|2.1.2|
+|Debian 9 ("stretch")|EOL|2.1.2|
 |Debian 8 ("jessie")|EOL|1.4.3|
 |Ubuntu 14.04 ("trusty")|EOL|1.4.3|
 |Ubuntu 12.04 ("precise")|EOL|8ed7b6dd5d4df93a3f37096afe6d6f81c2a7ef6e (2017-05-03)|
index d11fec9e1005e03140511d48fcee142f9e2a0e86..47d0c21beafcfb2bac714a75e561a340ffef029c 100644 (file)
@@ -84,7 +84,7 @@ Additional configuration settings are available:
 
 Check the LDAP section in the "default config file":{{site.baseurl}}/admin/config.html for more details and configuration options.
 
-h2(#pam). PAM (experimental)
+h2(#pam). PAM
 
 With this configuration, authentication is done according to the Linux PAM ("Pluggable Authentication Modules") configuration on your controller host.
 
@@ -98,8 +98,8 @@ Enable PAM authentication in @config.yml@:
 
 Check the "default config file":{{site.baseurl}}/admin/config.html for more PAM configuration options.
 
-The default PAM configuration on most Linux systems uses the local password database in @/etc/shadow@ for all logins. In this case, in order to log in to Arvados, users must have a UNIX account and password on the controller host itself. This can be convenient for a single-user or test cluster. User accounts can have @/dev/false@ as the shell in order to allow the user to log into Arvados but not log into a shell on the controller host.
+The default PAM configuration on most Linux systems uses the local user/password database in @/etc/passwd@ and @/etc/shadow@ for all logins. In this case, in order to log in to Arvados, users must have a UNIX account and password on the controller host itself. This can be convenient for a single-user or test cluster. Configuring a user account with a shell of @/bin/false@ will enable the user to log into Arvados but not log into shell login on the controller host.
 
-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.
+PAM can also be configured to use other authentication systems such such as NIS or Kerberos. In a production environment, PAM configuration should use the service name ("arvados" by default) and set a separate policy for Arvados login.  In this case, 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.
diff --git a/doc/user/cwl/costanalyzer.html.textile.liquid b/doc/user/cwl/costanalyzer.html.textile.liquid
new file mode 100644 (file)
index 0000000..fc39ada
--- /dev/null
@@ -0,0 +1,86 @@
+---
+layout: default
+navsection: userguide
+title: Analyzing workflow cost (cloud only)
+...
+{% comment %}
+Copyright (C) The Arvados Authors. All rights reserved.
+
+SPDX-License-Identifier: CC-BY-SA-3.0
+{% endcomment %}
+
+{% include 'notebox_begin' %}
+
+This is only applicable when Arvados runs in a cloud environment and @arvados-dispatch-cloud@ is used to dispatch @crunch@ jobs. The per node-hour price for each defined InstanceType most be supplied in "config.yml":{{site.baseurl}}/admin/config.html.
+
+{% include 'notebox_end' %}
+
+The @arvados-client@ program can be used to analyze the cost of a workflow. It can be installed from packages (@apt install arvados-client@ or @yum install arvados-client@). The @arvados-client costanalyzer@ command analyzes the cost accounting information associated with Arvados container requests.
+
+h2(#syntax). Syntax
+
+The @arvados-client costanalyzer@ tool has a number of command line arguments:
+
+<notextile>
+<pre><code>~$ <span class="userinput">arvados-client costanalyzer -h</span>
+Usage:
+  arvados-client costanalyzer [options ...] uuid [uuid ...]
+
+  This program analyzes the cost of Arvados container requests. For each uuid
+  supplied, it creates a CSV report that lists all the containers used to
+  fulfill the container request, together with the machine type and cost of
+  each container. At least one uuid must be specified.
+
+  When supplied with the uuid of a container request, it will calculate the
+  cost of that container request and all its children.
+
+  When supplied with the uuid of a collection, it will see if there is a
+  container_request uuid in the properties of the collection, and if so, it
+  will calculate the cost of that container request and all its children.
+
+  When supplied with a project uuid or when supplied with multiple container
+  request or collection uuids, it will create a CSV report for each supplied
+  uuid, as well as a CSV file with aggregate cost accounting for all supplied
+  uuids. The aggregate cost report takes container reuse into account: if a
+  container was reused between several container requests, its cost will only
+  be counted once.
+
+  Caveats:
+
+  - This program uses the cost data from config.yml at the time of the
+  execution of the container, stored in the 'node.json' file in its log
+  collection. If the cost data was not correctly configured at the time the
+  container was executed, the output from this program will be incorrect.
+
+  - If a container was run on a preemptible ("spot") instance, the cost data
+  reported by this program may be wildly inaccurate, because it does not have
+  access to the spot pricing in effect for the node then the container ran. The
+  UUID report file that is generated when the '-output' option is specified has
+  a column that indicates the preemptible state of the instance that ran the
+  container.
+
+  - This program does not take into account overhead costs like the time spent
+  starting and stopping compute nodes that run containers, the cost of the
+  permanent cloud nodes that provide the Arvados services, the cost of data
+  stored in Arvados, etc.
+
+  - When provided with a project uuid, subprojects will not be considered.
+
+  In order to get the data for the uuids supplied, the ARVADOS_API_HOST and
+  ARVADOS_API_TOKEN environment variables must be set.
+
+  This program prints the total dollar amount from the aggregate cost
+  accounting across all provided uuids on stdout.
+
+  When the '-output' option is specified, a set of CSV files with cost details
+  will be written to the provided directory.
+
+Options:
+  -cache
+      create and use a local disk cache of Arvados objects (default true)
+  -log-level level
+      logging level (debug, info, ...) (default "info")
+  -output directory
+      output directory for the CSV reports
+</code></pre>
+</notextile>
diff --git a/doc/user/debugging/container-shell-access.html.textile.liquid b/doc/user/debugging/container-shell-access.html.textile.liquid
new file mode 100644 (file)
index 0000000..91347e6
--- /dev/null
@@ -0,0 +1,79 @@
+---
+layout: default
+navsection: userguide
+title: Debugging workflows - shell access
+...
+{% comment %}
+Copyright (C) The Arvados Authors. All rights reserved.
+
+SPDX-License-Identifier: CC-BY-SA-3.0
+{% endcomment %}
+
+{% include 'notebox_begin' %}
+
+To use this feature, your Arvados installation must be configured to allow container shell access. See "the install guide":{{site.baseurl}}/install/container-shell-access.html for more information.
+
+{% include 'notebox_end' %}
+
+The @arvados-client@ program can be used to connect to a container in a running workflow. It can be installed from packages (@apt install arvados-client@ or @yum install arvados-client@). The @arvados-client shell@ command provides an ssh connection into a running container.
+
+h2(#syntax). Syntax
+
+The @arvados-client shell@ tool has the following syntax:
+
+<notextile>
+<pre><code>~$ <span class="userinput">arvados-client shell -h</span>
+arvados-client shell: open an interactive shell on a running container.
+
+Usage: arvados-client shell [options] [username@]container-uuid [ssh-options] [remote-command [args...]]
+
+Options:
+  -detach-keys string
+      set detach key sequence, as in docker-attach(1) (default "ctrl-],ctrl-]")
+
+</code></pre>
+</notextile>
+
+The @arvados-client shell@ command calls the ssh binary on your system to make the connection. Everything after _[username@]container-uuid_ is passed through to your OpenSSH client.
+
+h2(#Examples). Examples
+
+Connect to a running container, using the container request UUID:
+
+<notextile>
+<pre><code>~$ <span class="userinput">arvados-client shell ce8i5-xvhdp-e6wnujfslyyqn4b</span>
+root@0f13dcd755fa:~#
+</code></pre>
+</notextile>
+
+The container UUID also works:
+
+<notextile>
+<pre><code>~$ <span class="userinput">arvados-client shell ce8i5-dz642-h1cl0sa62d4i430</span>
+root@0f13dcd755fa:~#
+</code></pre>
+</notextile>
+
+SSH port forwarding is supported:
+
+<notextile>
+<pre><code>~$ <span class="userinput">arvados-client shell ce8i5-dz642-h1cl0sa62d4i430 -L8888:localhost:80</span>
+root@0f13dcd755fa:~# nc -l -p 80
+</code></pre>
+</notextile>
+
+And then, connecting to port 8888 locally:
+
+<notextile>
+<pre><code>~$ <span class="userinput">echo hello | nc localhost 8888</span>
+</code></pre>
+</notextile>
+
+Which appears on the other end:
+
+<notextile>
+<pre><code>~$ <span class="userinput">arvados-client shell ce8i5-dz642-h1cl0sa62d4i430 -L8888:localhost:80</span>
+root@0f13dcd755fa:~# nc -l -p 80
+hello
+</code></pre>
+</notextile>
index c94b4ee9a1da279e3237a7c152ed215a6257fc93..e24084ca07382bbab44fbdba0b19f4f1ef9e7208 100644 (file)
@@ -538,7 +538,7 @@ Clusters:
         UUIDTTL: 5s
 
         # Block cache entries. Each block consumes up to 64 MiB RAM.
-        MaxBlockEntries: 4
+        MaxBlockEntries: 20
 
         # Collection cache entries.
         MaxCollectionEntries: 1000
@@ -633,6 +633,23 @@ Clusters:
         AuthenticationRequestParameters:
           SAMPLE: ""
 
+        # Accept an OIDC access token as an API token if the OIDC
+        # provider's UserInfo endpoint accepts it.
+        #
+        # AcceptAccessTokenScope should also be used when enabling
+        # this feature.
+        AcceptAccessToken: false
+
+        # Before accepting an OIDC access token as an API token, first
+        # check that it is a JWT whose "scope" value includes this
+        # value. Example: "https://zzzzz.example.com/" (your Arvados
+        # API endpoint).
+        #
+        # If this value is empty and AcceptAccessToken is true, all
+        # access tokens will be accepted regardless of scope,
+        # including non-JWT tokens. This is not recommended.
+        AcceptAccessTokenScope: ""
+
       PAM:
         # (Experimental) Use PAM to authenticate users.
         Enable: false
index 9163421becb28c22539ffd2f41eb96bfb914d884..bd88c5d013af8df2546116e65a013d397ced4e6c 100644 (file)
@@ -158,6 +158,8 @@ var whitelist = map[string]bool{
        "Login.LDAP.UsernameAttribute":                        false,
        "Login.LoginCluster":                                  true,
        "Login.OpenIDConnect":                                 true,
+       "Login.OpenIDConnect.AcceptAccessToken":               false,
+       "Login.OpenIDConnect.AcceptAccessTokenScope":          false,
        "Login.OpenIDConnect.AuthenticationRequestParameters": false,
        "Login.OpenIDConnect.ClientID":                        false,
        "Login.OpenIDConnect.ClientSecret":                    false,
index e344d226652038275a2231b21ba91e2e1984416e..9f4bf011b8dbf02cf19ec4c61cc919072baf1d08 100644 (file)
@@ -544,7 +544,7 @@ Clusters:
         UUIDTTL: 5s
 
         # Block cache entries. Each block consumes up to 64 MiB RAM.
-        MaxBlockEntries: 4
+        MaxBlockEntries: 20
 
         # Collection cache entries.
         MaxCollectionEntries: 1000
@@ -639,6 +639,23 @@ Clusters:
         AuthenticationRequestParameters:
           SAMPLE: ""
 
+        # Accept an OIDC access token as an API token if the OIDC
+        # provider's UserInfo endpoint accepts it.
+        #
+        # AcceptAccessTokenScope should also be used when enabling
+        # this feature.
+        AcceptAccessToken: false
+
+        # Before accepting an OIDC access token as an API token, first
+        # check that it is a JWT whose "scope" value includes this
+        # value. Example: "https://zzzzz.example.com/" (your Arvados
+        # API endpoint).
+        #
+        # If this value is empty and AcceptAccessToken is true, all
+        # access tokens will be accepted regardless of scope,
+        # including non-JWT tokens. This is not recommended.
+        AcceptAccessTokenScope: ""
+
       PAM:
         # (Experimental) Use PAM to authenticate users.
         Enable: false
index 01990620f6094dd10063df7e5e9410e082cade36..69458655ba0c8ce7caf9839d05eb5985f3fd0b7b 100644 (file)
@@ -94,6 +94,8 @@ func (s *AuthSuite) SetUpTest(c *check.C) {
        cluster.Login.OpenIDConnect.ClientSecret = s.fakeProvider.ValidClientSecret
        cluster.Login.OpenIDConnect.EmailClaim = "email"
        cluster.Login.OpenIDConnect.EmailVerifiedClaim = "email_verified"
+       cluster.Login.OpenIDConnect.AcceptAccessToken = true
+       cluster.Login.OpenIDConnect.AcceptAccessTokenScope = ""
 
        s.testHandler = &Handler{Cluster: cluster}
        s.testServer = newServerFromIntegrationTestEnv(c)
index 183557eb15a4780bbe3388e3185ae9064dc609e3..039caac574e479bdad181dfeed745dd3255640cf 100644 (file)
@@ -113,6 +113,11 @@ func (conn *Conn) splitListRequest(ctx context.Context, opts arvados.ListOptions
                _, err := fn(ctx, conn.cluster.ClusterID, conn.local, opts)
                return err
        }
+       if opts.ClusterID != "" {
+               // Client explicitly selected cluster
+               _, err := fn(ctx, conn.cluster.ClusterID, conn.chooseBackend(opts.ClusterID), opts)
+               return err
+       }
 
        cannotSplit := false
        var matchAllFilters map[string]bool
index 7b1dcbea6655bcf5f86b175c58ad2f9d5382b74d..44c99bf30f8c3a6ae9aa70b8306268b7c4c8fb6d 100644 (file)
@@ -113,6 +113,8 @@ func (s *IntegrationSuite) SetUpSuite(c *check.C) {
         ClientSecret: ` + s.oidcprovider.ValidClientSecret + `
         EmailClaim: email
         EmailVerifiedClaim: email_verified
+        AcceptAccessToken: true
+        AcceptAccessTokenScope: ""
 `
                } else {
                        yaml += `
index 01fa84ea4fe885ba59e7f56099cdfb41d21e2a8c..0d6f2ef027e8500c60fdf644e8f808fecd2f226a 100644 (file)
@@ -54,15 +54,17 @@ func chooseLoginController(cluster *arvados.Cluster, parent *Conn) loginControll
                }
        case wantOpenIDConnect:
                return &oidcLoginController{
-                       Cluster:            cluster,
-                       Parent:             parent,
-                       Issuer:             cluster.Login.OpenIDConnect.Issuer,
-                       ClientID:           cluster.Login.OpenIDConnect.ClientID,
-                       ClientSecret:       cluster.Login.OpenIDConnect.ClientSecret,
-                       AuthParams:         cluster.Login.OpenIDConnect.AuthenticationRequestParameters,
-                       EmailClaim:         cluster.Login.OpenIDConnect.EmailClaim,
-                       EmailVerifiedClaim: cluster.Login.OpenIDConnect.EmailVerifiedClaim,
-                       UsernameClaim:      cluster.Login.OpenIDConnect.UsernameClaim,
+                       Cluster:                cluster,
+                       Parent:                 parent,
+                       Issuer:                 cluster.Login.OpenIDConnect.Issuer,
+                       ClientID:               cluster.Login.OpenIDConnect.ClientID,
+                       ClientSecret:           cluster.Login.OpenIDConnect.ClientSecret,
+                       AuthParams:             cluster.Login.OpenIDConnect.AuthenticationRequestParameters,
+                       EmailClaim:             cluster.Login.OpenIDConnect.EmailClaim,
+                       EmailVerifiedClaim:     cluster.Login.OpenIDConnect.EmailVerifiedClaim,
+                       UsernameClaim:          cluster.Login.OpenIDConnect.UsernameClaim,
+                       AcceptAccessToken:      cluster.Login.OpenIDConnect.AcceptAccessToken,
+                       AcceptAccessTokenScope: cluster.Login.OpenIDConnect.AcceptAccessTokenScope,
                }
        case wantSSO:
                return &ssoLoginController{Parent: parent}
index a435b014d967deafae3a72060809a3e843ecc975..61dc5c816b35661f39c4a800ab17f1bf55325f06 100644 (file)
@@ -35,6 +35,7 @@ import (
        "golang.org/x/oauth2"
        "google.golang.org/api/option"
        "google.golang.org/api/people/v1"
+       "gopkg.in/square/go-jose.v2/jwt"
 )
 
 var (
@@ -45,16 +46,18 @@ var (
 )
 
 type oidcLoginController struct {
-       Cluster            *arvados.Cluster
-       Parent             *Conn
-       Issuer             string // OIDC issuer URL, e.g., "https://accounts.google.com"
-       ClientID           string
-       ClientSecret       string
-       UseGooglePeopleAPI bool              // Use Google People API to look up alternate email addresses
-       EmailClaim         string            // OpenID claim to use as email address; typically "email"
-       EmailVerifiedClaim string            // If non-empty, ensure claim value is true before accepting EmailClaim; typically "email_verified"
-       UsernameClaim      string            // If non-empty, use as preferred username
-       AuthParams         map[string]string // Additional parameters to pass with authentication request
+       Cluster                *arvados.Cluster
+       Parent                 *Conn
+       Issuer                 string // OIDC issuer URL, e.g., "https://accounts.google.com"
+       ClientID               string
+       ClientSecret           string
+       UseGooglePeopleAPI     bool              // Use Google People API to look up alternate email addresses
+       EmailClaim             string            // OpenID claim to use as email address; typically "email"
+       EmailVerifiedClaim     string            // If non-empty, ensure claim value is true before accepting EmailClaim; typically "email_verified"
+       UsernameClaim          string            // If non-empty, use as preferred username
+       AcceptAccessToken      bool              // Accept access tokens as API tokens
+       AcceptAccessTokenScope string            // If non-empty, don't accept access tokens as API tokens unless they contain this scope
+       AuthParams             map[string]string // Additional parameters to pass with authentication request
 
        // override Google People API base URL for testing purposes
        // (normally empty, set by google pkg to
@@ -134,6 +137,7 @@ func (ctrl *oidcLoginController) Login(ctx context.Context, opts arvados.LoginOp
        if !ok {
                return loginError(errors.New("error in OAuth2 exchange: no ID token in OAuth2 token"))
        }
+       ctxlog.FromContext(ctx).WithField("rawIDToken", rawIDToken).Debug("oauth2Token provided ID token")
        idToken, err := ctrl.verifier.Verify(ctx, rawIDToken)
        if err != nil {
                return loginError(fmt.Errorf("error verifying ID token: %s", err))
@@ -448,6 +452,10 @@ func (ta *oidcTokenAuthorizer) registerToken(ctx context.Context, tok string) er
        if err != nil {
                return fmt.Errorf("error setting up OpenID Connect provider: %s", err)
        }
+       if ok, err := ta.checkAccessTokenScope(ctx, tok); err != nil || !ok {
+               ta.cache.Add(tok, time.Now().Add(tokenCacheNegativeTTL))
+               return err
+       }
        oauth2Token := &oauth2.Token{
                AccessToken: tok,
        }
@@ -494,3 +502,38 @@ func (ta *oidcTokenAuthorizer) registerToken(ctx context.Context, tok string) er
        ta.cache.Add(tok, aca)
        return nil
 }
+
+// Check that the provided access token is a JWT with the required
+// scope. If it is a valid JWT but missing the required scope, we
+// return a 403 error, otherwise true (acceptable as an API token) or
+// false (pass through unmodified).
+//
+// Return false if configured not to accept access tokens at all.
+//
+// Note we don't check signature or expiry here. We are relying on the
+// caller to verify those separately (e.g., by calling the UserInfo
+// endpoint).
+func (ta *oidcTokenAuthorizer) checkAccessTokenScope(ctx context.Context, tok string) (bool, error) {
+       if !ta.ctrl.AcceptAccessToken {
+               return false, nil
+       } else if ta.ctrl.AcceptAccessTokenScope == "" {
+               return true, nil
+       }
+       var claims struct {
+               Scope string `json:"scope"`
+       }
+       if t, err := jwt.ParseSigned(tok); err != nil {
+               ctxlog.FromContext(ctx).WithError(err).Debug("error parsing jwt")
+               return false, nil
+       } else if err = t.UnsafeClaimsWithoutVerification(&claims); err != nil {
+               ctxlog.FromContext(ctx).WithError(err).Debug("error extracting jwt claims")
+               return false, nil
+       }
+       for _, s := range strings.Split(claims.Scope, " ") {
+               if s == ta.ctrl.AcceptAccessTokenScope {
+                       return true, nil
+               }
+       }
+       ctxlog.FromContext(ctx).WithFields(logrus.Fields{"have": claims.Scope, "need": ta.ctrl.AcceptAccessTokenScope}).Infof("unacceptable access token scope")
+       return false, httpserver.ErrorWithStatus(errors.New("unacceptable access token scope"), http.StatusUnauthorized)
+}
index e3c72adddcdbbf76650fada2a8eb8401add88431..c9d6133c480319b9129397ea076068d67bb4a3f5 100644 (file)
@@ -208,22 +208,25 @@ func (s *OIDCLoginSuite) TestOIDCAuthorizer(c *check.C) {
        json.Unmarshal([]byte(fmt.Sprintf("%q", s.fakeProvider.Issuer.URL)), &s.cluster.Login.OpenIDConnect.Issuer)
        s.cluster.Login.OpenIDConnect.ClientID = "oidc#client#id"
        s.cluster.Login.OpenIDConnect.ClientSecret = "oidc#client#secret"
+       s.cluster.Login.OpenIDConnect.AcceptAccessToken = true
+       s.cluster.Login.OpenIDConnect.AcceptAccessTokenScope = ""
        s.fakeProvider.ValidClientID = "oidc#client#id"
        s.fakeProvider.ValidClientSecret = "oidc#client#secret"
        db := arvadostest.DB(c, s.cluster)
 
        tokenCacheTTL = time.Millisecond
        tokenCacheRaceWindow = time.Millisecond
+       tokenCacheNegativeTTL = time.Millisecond
 
        oidcAuthorizer := OIDCAccessTokenAuthorizer(s.cluster, func(context.Context) (*sqlx.DB, error) { return db, nil })
        accessToken := s.fakeProvider.ValidAccessToken()
 
        mac := hmac.New(sha256.New, []byte(s.cluster.SystemRootToken))
        io.WriteString(mac, accessToken)
-       hmac := fmt.Sprintf("%x", mac.Sum(nil))
+       apiToken := fmt.Sprintf("%x", mac.Sum(nil))
 
        cleanup := func() {
-               _, err := db.Exec(`delete from api_client_authorizations where api_token=$1`, hmac)
+               _, err := db.Exec(`delete from api_client_authorizations where api_token=$1`, apiToken)
                c.Check(err, check.IsNil)
        }
        cleanup()
@@ -237,7 +240,7 @@ func (s *OIDCLoginSuite) TestOIDCAuthorizer(c *check.C) {
                c.Assert(creds.Tokens, check.HasLen, 1)
                c.Check(creds.Tokens[0], check.Equals, accessToken)
 
-               err := db.QueryRowContext(ctx, `select expires_at at time zone 'UTC' from api_client_authorizations where api_token=$1`, hmac).Scan(&exp1)
+               err := db.QueryRowContext(ctx, `select expires_at at time zone 'UTC' from api_client_authorizations where api_token=$1`, apiToken).Scan(&exp1)
                c.Check(err, check.IsNil)
                c.Check(exp1.Sub(time.Now()) > -time.Second, check.Equals, true)
                c.Check(exp1.Sub(time.Now()) < time.Second, check.Equals, true)
@@ -245,17 +248,58 @@ func (s *OIDCLoginSuite) TestOIDCAuthorizer(c *check.C) {
        })(ctx, nil)
 
        // If the token is used again after the in-memory cache
-       // expires, oidcAuthorizer must re-checks the token and update
+       // expires, oidcAuthorizer must re-check the token and update
        // the expires_at value in the database.
        time.Sleep(3 * time.Millisecond)
        oidcAuthorizer.WrapCalls(func(ctx context.Context, opts interface{}) (interface{}, error) {
                var exp time.Time
-               err := db.QueryRowContext(ctx, `select expires_at at time zone 'UTC' from api_client_authorizations where api_token=$1`, hmac).Scan(&exp)
+               err := db.QueryRowContext(ctx, `select expires_at at time zone 'UTC' from api_client_authorizations where api_token=$1`, apiToken).Scan(&exp)
                c.Check(err, check.IsNil)
                c.Check(exp.Sub(exp1) > 0, check.Equals, true)
                c.Check(exp.Sub(exp1) < time.Second, check.Equals, true)
                return nil, nil
        })(ctx, nil)
+
+       s.fakeProvider.AccessTokenPayload = map[string]interface{}{"scope": "openid profile foobar"}
+       accessToken = s.fakeProvider.ValidAccessToken()
+       ctx = auth.NewContext(context.Background(), &auth.Credentials{Tokens: []string{accessToken}})
+
+       mac = hmac.New(sha256.New, []byte(s.cluster.SystemRootToken))
+       io.WriteString(mac, accessToken)
+       apiToken = fmt.Sprintf("%x", mac.Sum(nil))
+
+       for _, trial := range []struct {
+               configEnable bool
+               configScope  string
+               acceptable   bool
+               shouldRun    bool
+       }{
+               {true, "foobar", true, true},
+               {true, "foo", false, false},
+               {true, "", true, true},
+               {false, "", false, true},
+               {false, "foobar", false, true},
+       } {
+               c.Logf("trial = %+v", trial)
+               cleanup()
+               s.cluster.Login.OpenIDConnect.AcceptAccessToken = trial.configEnable
+               s.cluster.Login.OpenIDConnect.AcceptAccessTokenScope = trial.configScope
+               oidcAuthorizer = OIDCAccessTokenAuthorizer(s.cluster, func(context.Context) (*sqlx.DB, error) { return db, nil })
+               checked := false
+               oidcAuthorizer.WrapCalls(func(ctx context.Context, opts interface{}) (interface{}, error) {
+                       var n int
+                       err := db.QueryRowContext(ctx, `select count(*) from api_client_authorizations where api_token=$1`, apiToken).Scan(&n)
+                       c.Check(err, check.IsNil)
+                       if trial.acceptable {
+                               c.Check(n, check.Equals, 1)
+                       } else {
+                               c.Check(n, check.Equals, 0)
+                       }
+                       checked = true
+                       return nil, nil
+               })(ctx, nil)
+               c.Check(checked, check.Equals, trial.shouldRun)
+       }
 }
 
 func (s *OIDCLoginSuite) TestGenericOIDCLogin(c *check.C) {
index 37e655e53a3391b8820a3855efeb2ca891dcd5f8..402e0ec81a160e677a1aefb8df82d589f01c3e20 100644 (file)
@@ -57,7 +57,7 @@ func parseFlags(prog string, args []string, loader *config.Loader, logger *logru
        flags.Usage = func() {
                fmt.Fprintf(flags.Output(), `
 Usage:
-  %s [options ...] <uuid> ...
+  %s [options ...] uuid [uuid ...]
 
        This program analyzes the cost of Arvados container requests. For each uuid
        supplied, it creates a CSV report that lists all the containers used to
@@ -78,26 +78,27 @@ Usage:
        container was reused between several container requests, its cost will only
        be counted once.
 
-       To get the node costs, the progam queries the Arvados API for current cost
-       data for each node type used. This means that the reported cost always
-       reflects the cost data as currently defined in the Arvados API configuration
-       file.
-
        Caveats:
-       - the Arvados API configuration cost data may be out of sync with the cloud
-       provider.
-       - when generating reports for older container requests, the cost data in the
-       Arvados API configuration file may have changed since the container request
-       was fulfilled. This program uses the cost data stored at the time of the
+
+       - This program uses the cost data from config.yml at the time of the
        execution of the container, stored in the 'node.json' file in its log
-       collection.
-       - if a container was run on a preemptible ("spot") instance, the cost data
+       collection. If the cost data was not correctly configured at the time the
+       container was executed, the output from this program will be incorrect.
+
+       - If a container was run on a preemptible ("spot") instance, the cost data
        reported by this program may be wildly inaccurate, because it does not have
        access to the spot pricing in effect for the node then the container ran. The
        UUID report file that is generated when the '-output' option is specified has
        a column that indicates the preemptible state of the instance that ran the
        container.
 
+       - This program does not take into account overhead costs like the time spent
+       starting and stopping compute nodes that run containers, the cost of the
+       permanent cloud nodes that provide the Arvados services, the cost of data
+       stored in Arvados, etc.
+
+       - When provided with a project uuid, subprojects will not be considered.
+
        In order to get the data for the uuids supplied, the ARVADOS_API_HOST and
        ARVADOS_API_TOKEN environment variables must be set.
 
index 78e5f4faa8074576da0b9c5b6046e5bce3a90c80..72c714dfa4ef47bbe4d60adff4693edd97e7b7cb 100644 (file)
@@ -331,8 +331,8 @@ func (cp *copier) walkHostFS(dest, src string, maxSymlinks int, includeMounts bo
                })
                return nil
        }
-
-       return fmt.Errorf("Unsupported file type (mode %o) in output dir: %q", fi.Mode(), src)
+       cp.logger.Printf("Skipping unsupported file type (mode %o) in output dir: %q", fi.Mode(), src)
+       return nil
 }
 
 // Return the host path that was mounted at the given path in the
index 9135bcaa1d87808ba64f54594aa6e902b0870c19..5e92490163f6e34bc935eae42d2002fdea74436f 100644 (file)
@@ -5,27 +5,31 @@
 package crunchrun
 
 import (
+       "bytes"
        "io"
        "io/ioutil"
        "os"
+       "syscall"
 
        "git.arvados.org/arvados.git/sdk/go/arvados"
        "git.arvados.org/arvados.git/sdk/go/arvadosclient"
        "git.arvados.org/arvados.git/sdk/go/arvadostest"
+       "github.com/sirupsen/logrus"
        check "gopkg.in/check.v1"
 )
 
 var _ = check.Suite(&copierSuite{})
 
 type copierSuite struct {
-       cp copier
+       cp  copier
+       log bytes.Buffer
 }
 
 func (s *copierSuite) SetUpTest(c *check.C) {
-       tmpdir, err := ioutil.TempDir("", "crunch-run.test.")
-       c.Assert(err, check.IsNil)
+       tmpdir := c.MkDir()
        api, err := arvadosclient.MakeArvadosClient()
        c.Assert(err, check.IsNil)
+       s.log = bytes.Buffer{}
        s.cp = copier{
                client:        arvados.NewClientFromEnv(),
                arvClient:     api,
@@ -37,13 +41,10 @@ func (s *copierSuite) SetUpTest(c *check.C) {
                secretMounts: map[string]arvados.Mount{
                        "/secret_text": {Kind: "text", Content: "xyzzy"},
                },
+               logger: &logrus.Logger{Out: &s.log, Formatter: &logrus.TextFormatter{}, Level: logrus.InfoLevel},
        }
 }
 
-func (s *copierSuite) TearDownTest(c *check.C) {
-       os.RemoveAll(s.cp.hostOutputDir)
-}
-
 func (s *copierSuite) TestEmptyOutput(c *check.C) {
        err := s.cp.walkMount("", s.cp.ctrOutputDir, 10, true)
        c.Check(err, check.IsNil)
@@ -59,6 +60,8 @@ func (s *copierSuite) TestRegularFilesAndDirs(c *check.C) {
        _, err = io.WriteString(f, "foo")
        c.Assert(err, check.IsNil)
        c.Assert(f.Close(), check.IsNil)
+       err = syscall.Mkfifo(s.cp.hostOutputDir+"/dir1/fifo", 0644)
+       c.Assert(err, check.IsNil)
 
        err = s.cp.walkMount("", s.cp.ctrOutputDir, 10, true)
        c.Check(err, check.IsNil)
@@ -67,6 +70,7 @@ func (s *copierSuite) TestRegularFilesAndDirs(c *check.C) {
                {src: os.DevNull, dst: "/dir1/dir2/dir3/.keep"},
                {src: s.cp.hostOutputDir + "/dir1/foo", dst: "/dir1/foo", size: 3},
        })
+       c.Check(s.log.String(), check.Matches, `.* msg="Skipping unsupported file type \(mode 200000644\) in output dir: \\"/ctr/outdir/dir1/fifo\\""\n`)
 }
 
 func (s *copierSuite) TestSymlinkCycle(c *check.C) {
index 2ff43232185068683d603bb49768068c818602a3..b1ee6e9b992f4446ec1f0f826c22e9bdb244704b 100644 (file)
@@ -167,6 +167,8 @@ type Cluster struct {
                        EmailClaim                      string
                        EmailVerifiedClaim              string
                        UsernameClaim                   string
+                       AcceptAccessToken               bool
+                       AcceptAccessTokenScope          string
                        AuthenticationRequestParameters map[string]string
                }
                PAM struct {
index 96205f919fa79b813721af4304bdbc27084e4b7f..de21302e5a048dfbca340abf24cb6c5359de7305 100644 (file)
@@ -17,6 +17,7 @@ import (
 
        "gopkg.in/check.v1"
        "gopkg.in/square/go-jose.v2"
+       "gopkg.in/square/go-jose.v2/jwt"
 )
 
 type OIDCProvider struct {
@@ -25,9 +26,10 @@ type OIDCProvider struct {
        ValidClientID     string
        ValidClientSecret string
        // desired response from token endpoint
-       AuthEmail         string
-       AuthEmailVerified bool
-       AuthName          string
+       AuthEmail          string
+       AuthEmailVerified  bool
+       AuthName           string
+       AccessTokenPayload map[string]interface{}
 
        PeopleAPIResponse map[string]interface{}
 
@@ -44,11 +46,13 @@ func NewOIDCProvider(c *check.C) *OIDCProvider {
        c.Assert(err, check.IsNil)
        p.Issuer = httptest.NewServer(http.HandlerFunc(p.serveOIDC))
        p.PeopleAPI = httptest.NewServer(http.HandlerFunc(p.servePeopleAPI))
+       p.AccessTokenPayload = map[string]interface{}{"sub": "example"}
        return p
 }
 
 func (p *OIDCProvider) ValidAccessToken() string {
-       return p.fakeToken([]byte("fake access token"))
+       buf, _ := json.Marshal(p.AccessTokenPayload)
+       return p.fakeToken(buf)
 }
 
 func (p *OIDCProvider) serveOIDC(w http.ResponseWriter, req *http.Request) {
@@ -118,7 +122,8 @@ func (p *OIDCProvider) serveOIDC(w http.ResponseWriter, req *http.Request) {
        case "/auth":
                w.WriteHeader(http.StatusInternalServerError)
        case "/userinfo":
-               if authhdr := req.Header.Get("Authorization"); strings.TrimPrefix(authhdr, "Bearer ") != p.ValidAccessToken() {
+               authhdr := req.Header.Get("Authorization")
+               if _, err := jwt.ParseSigned(strings.TrimPrefix(authhdr, "Bearer ")); err != nil {
                        p.c.Logf("OIDCProvider: bad auth %q", authhdr)
                        w.WriteHeader(http.StatusUnauthorized)
                        return
index 5dbdb07f2ce11c3abdb54d1fb7bd02368874e0be..58504d057a9a085f9afbc7a24dff161d8f96b340 100644 (file)
@@ -154,7 +154,7 @@ GEM
     mimemagic (0.3.8)
       nokogiri (~> 1)
     mini_mime (1.0.2)
-    mini_portile2 (2.5.0)
+    mini_portile2 (2.5.1)
     minitest (5.10.3)
     mocha (1.8.0)
       metaclass (~> 0.0.1)
@@ -170,7 +170,7 @@ GEM
     net-ssh-gateway (2.0.0)
       net-ssh (>= 4.0.0)
     nio4r (2.5.7)
-    nokogiri (1.11.2)
+    nokogiri (1.11.5)
       mini_portile2 (~> 2.5.0)
       racc (~> 1.4)
     oauth2 (1.4.1)
index 07db7a016f7bbd25442b4b7500e53633bd4b0059..9bdecdca1c40cfd2662197e39f4c129fc146932e 100644 (file)
@@ -195,7 +195,7 @@ func (c *cache) Update(client *arvados.Client, coll arvados.Collection, fs arvad
                },
        })
        if err == nil {
-               c.collections.Add(client.AuthToken+"\000"+coll.PortableDataHash, &cachedCollection{
+               c.collections.Add(client.AuthToken+"\000"+updated.PortableDataHash, &cachedCollection{
                        expire:     time.Now().Add(time.Duration(c.config.TTL)),
                        collection: &updated,
                })
index 3ff7cb1926b69d36ccd8f683ec544ec977fd7aef..446d591bfd715224651c1d9667e0c451e81f664e 100644 (file)
@@ -1118,6 +1118,62 @@ func (s *IntegrationSuite) TestKeepClientBlockCache(c *check.C) {
        c.Check(keepclient.DefaultBlockCache.MaxBlocks, check.Equals, 42)
 }
 
+// Writing to a collection shouldn't affect its entry in the
+// PDH-to-manifest cache.
+func (s *IntegrationSuite) TestCacheWriteCollectionSamePDH(c *check.C) {
+       arv, err := arvadosclient.MakeArvadosClient()
+       c.Assert(err, check.Equals, nil)
+       arv.ApiToken = arvadostest.ActiveToken
+
+       u := mustParseURL("http://x.example/testfile")
+       req := &http.Request{
+               Method:     "GET",
+               Host:       u.Host,
+               URL:        u,
+               RequestURI: u.RequestURI(),
+               Header:     http.Header{"Authorization": {"Bearer " + arv.ApiToken}},
+       }
+
+       checkWithID := func(id string, status int) {
+               req.URL.Host = strings.Replace(id, "+", "-", -1) + ".example"
+               req.Host = req.URL.Host
+               resp := httptest.NewRecorder()
+               s.testServer.Handler.ServeHTTP(resp, req)
+               c.Check(resp.Code, check.Equals, status)
+       }
+
+       var colls [2]arvados.Collection
+       for i := range colls {
+               err := arv.Create("collections",
+                       map[string]interface{}{
+                               "ensure_unique_name": true,
+                               "collection": map[string]interface{}{
+                                       "name": "test collection",
+                               },
+                       }, &colls[i])
+               c.Assert(err, check.Equals, nil)
+       }
+
+       // Populate cache with empty collection
+       checkWithID(colls[0].PortableDataHash, http.StatusNotFound)
+
+       // write a file to colls[0]
+       reqPut := *req
+       reqPut.Method = "PUT"
+       reqPut.URL.Host = colls[0].UUID + ".example"
+       reqPut.Host = req.URL.Host
+       reqPut.Body = ioutil.NopCloser(bytes.NewBufferString("testdata"))
+       resp := httptest.NewRecorder()
+       s.testServer.Handler.ServeHTTP(resp, &reqPut)
+       c.Check(resp.Code, check.Equals, http.StatusCreated)
+
+       // new file should not appear in colls[1]
+       checkWithID(colls[1].PortableDataHash, http.StatusNotFound)
+       checkWithID(colls[1].UUID, http.StatusNotFound)
+
+       checkWithID(colls[0].UUID, http.StatusOK)
+}
+
 func copyHeader(h http.Header) http.Header {
        hc := http.Header{}
        for k, v := range h {
index 00d486e1cd83ca42e2cf56bcd81da0df5cd8fb6f..dc9043217ed20bdef72c17546e4072cd485fef9b 100644 (file)
@@ -7,7 +7,7 @@ The nodes requiring certificates are:
 
 * CLUSTER.DOMAIN
 * collections.CLUSTER.DOMAIN
-* \*\-\-collections.CLUSTER.DOMAIN
+* \*.collections.CLUSTER.DOMAIN
 * download.CLUSTER.DOMAIN
 * keep.CLUSTER.DOMAIN
 * workbench.CLUSTER.DOMAIN
index 4ecc65e28f0f97c4702f2a10cfc34a7bb828d5c0..f7052efc105abcce54b1e50aa6b294debacf13b8 100644 (file)
@@ -175,7 +175,7 @@ arvados:
         InternalURLs:
           'http://localhost:8004': {}
       WebDAV:
-        ExternalURL: 'https://*--collections.__CLUSTER__.__DOMAIN__:__KEEPWEB_EXT_SSL_PORT__/'
+        ExternalURL: 'https://*.collections.__CLUSTER__.__DOMAIN__:__KEEPWEB_EXT_SSL_PORT__/'
         InternalURLs:
           'http://localhost:9002': {}
       WebDAVDownload:
index 6ba8b9b099d9e3168d3996ae0c88f2e83b78e47f..90593307d3a1916c74fac32f001ec86d82b50302 100644 (file)
@@ -8,23 +8,13 @@ letsencrypt:
   use_package: true
   pkgs:
     - certbot: latest
-    - python3-certbot-nginx
+    - python3-certbot-dns-route53
   config:
     server: https://acme-v02.api.letsencrypt.org/directory
     email: __INITIAL_USER_EMAIL__
-    authenticator: nginx
-    webroot-path: /var/www
+    authenticator: dns-route53
     agree-tos: true
     keep-until-expiring: true
     expand: true
     max-log-backups: 0
     deploy-hook: systemctl reload nginx
-
-### NGINX
-nginx:
-  ### SNIPPETS
-  snippets:
-    ### LETSENCRYPT DEFAULT PATH
-    letsencrypt_well_known.conf:
-      - location /.well-known:
-        - root: /var/www
index dc34ea6fd5f0094c378062ed09e299bf2e78b6be..35ec9b0da734492d1237119c1fe2b0cf862a2006 100644 (file)
@@ -10,6 +10,7 @@ letsencrypt:
       - download.__CLUSTER__.__DOMAIN__
     collections.__CLUSTER__.__DOMAIN__:
       - collections.__CLUSTER__.__DOMAIN__
+      - *.collections.__CLUSTER__.__DOMAIN__
 
 ### NGINX
 nginx:
index 3be1696602459a68598d3f4e2b44c524dc7f3d06..aa11cca74e7ff5d4b6558255b2596b9c47dc850d 100644 (file)
@@ -29,7 +29,6 @@ nginx:
             - server_name: __CLUSTER__.__DOMAIN__
             - listen:
               - 80 default
-            - include: snippets/letsencrypt_well_known.conf
             - location /:
               - return: '301 https://$host$request_uri'
 
index 5d8b37e595a5a0d17d981c62b1bf1a5f68728f82..fac97f3c6c4cfb32a6b569eb9d4fd14669d6ef59 100644 (file)
@@ -24,7 +24,6 @@ nginx:
             - server_name: keep.__CLUSTER__.__DOMAIN__
             - listen:
               - 80
-            - include: snippets/letsencrypt_well_known.conf
             - location /:
               - return: '301 https://$host$request_uri'
 
index fca42160766215c04663198ce1d1ed25bf32d5be..5a25ae899106bca90d4fa3e4fedf5a8eccea4170 100644 (file)
@@ -21,10 +21,9 @@ nginx:
         overwrite: true
         config:
           - server:
-            - server_name: '~^((.*--)?collections|download)\.__CLUSTER__\.__DOMAIN__'
+            - server_name: '~^((.*\.)?collections|download)\.__CLUSTER__\.__DOMAIN__'
             - listen:
               - 80
-            - include: snippets/letsencrypt_well_known.conf
             - location /:
               - return: '301 https://$host$request_uri'
 
@@ -36,7 +35,7 @@ nginx:
           cmd: create-initial-cert-collections.__CLUSTER__.__DOMAIN__-collections.__CLUSTER__.__DOMAIN__
         config:
           - server:
-            - server_name: '~^(.*--)?collections\.__CLUSTER__\.__DOMAIN__'
+            - server_name: '*.collections.__CLUSTER__.__DOMAIN__'
             - listen:
               - __CONTROLLER_EXT_SSL_PORT__ http2 ssl
             - index: index.html index.htm
index 46f8ad0386aa00f96fc720db7ff2c3f2aa52da66..49c86dd313c22041f1b8001a13d13ddcbbeb3319 100644 (file)
@@ -25,7 +25,6 @@ nginx:
             - server_name: webshell.__CLUSTER__.__DOMAIN__
             - listen:
               - 80
-            - include: snippets/letsencrypt_well_known.conf
             - location /:
               - return: '301 https://$host$request_uri'
 
index e89b780da64d122fa6c3f64d6a20f0c7f70f735d..c9671cd0c263625a7262677f27abe00bb95051a5 100644 (file)
@@ -24,7 +24,6 @@ nginx:
             - server_name: ws.__CLUSTER__.__DOMAIN__
             - listen:
               - 80
-            - include: snippets/letsencrypt_well_known.conf
             - location /:
               - return: '301 https://$host$request_uri'
 
index a3e58e2e25dc040fe718ce61b69c0b503bc981a6..bd4123539e4192f323a802c57161084829a36e2c 100644 (file)
@@ -22,7 +22,6 @@ nginx:
             - server_name: workbench2.__CLUSTER__.__DOMAIN__
             - listen:
               - 80
-            - include: snippets/letsencrypt_well_known.conf
             - location /:
               - return: '301 https://$host$request_uri'
 
index 38e59cc1ba1f4c274205a7d9b080e7238babe457..ec28b98c60da3930d3bb28db35e8c9dfd4fa44b5 100644 (file)
@@ -31,7 +31,6 @@ nginx:
             - server_name: workbench.__CLUSTER__.__DOMAIN__
             - listen:
               - 80
-            - include: snippets/letsencrypt_well_known.conf
             - location /:
               - return: '301 https://$host$request_uri'
 
index 02da9933bdeab991415f4956257f5a9c22a5abf4..5cef4d0fc5869ba0e37341583e04b5838cbc11e9 100755 (executable)
@@ -242,7 +242,7 @@ if [ ! -d "${SOURCE_PILLARS_DIR}" ]; then
   echo "${SOURCE_PILLARS_DIR} does not exist or is not a directory. Exiting."
   exit 1
 fi
-for f in "${SOURCE_PILLARS_DIR}"/*; do
+for f in $(ls "${SOURCE_PILLARS_DIR}"/*); do
   sed "s#__ANONYMOUS_USER_TOKEN__#${ANONYMOUS_USER_TOKEN}#g;
        s#__BLOB_SIGNING_KEY__#${BLOB_SIGNING_KEY}#g;
        s#__CONTROLLER_EXT_SSL_PORT__#${CONTROLLER_EXT_SSL_PORT}#g;
@@ -285,7 +285,7 @@ if [ "x${TEST}" = "xyes" ] && [ ! -d "${SOURCE_TESTS_DIR}" ]; then
 fi
 mkdir -p /tmp/cluster_tests
 # Replace cluster and domain name in the test files
-for f in "${SOURCE_TESTS_DIR}"/*; do
+for f in $(ls "${SOURCE_TESTS_DIR}"/*); do
   sed "s#__CLUSTER__#${CLUSTER}#g;
        s#__CONTROLLER_EXT_SSL_PORT__#${CONTROLLER_EXT_SSL_PORT}#g;
        s#__DOMAIN__#${DOMAIN}#g;
@@ -303,7 +303,7 @@ chmod 755 /tmp/cluster_tests/run-test.sh
 if [ -d "${SOURCE_STATES_DIR}" ]; then
   mkdir -p "${F_DIR}"/extra/extra
 
-  for f in "${SOURCE_STATES_DIR}"/*; do
+  for f in $(ls "${SOURCE_STATES_DIR}"/*); do
     sed "s#__ANONYMOUS_USER_TOKEN__#${ANONYMOUS_USER_TOKEN}#g;
          s#__CLUSTER__#${CLUSTER}#g;
          s#__BLOB_SIGNING_KEY__#${BLOB_SIGNING_KEY}#g;
@@ -362,7 +362,7 @@ EOFPSLS
 
 # States, extra states
 if [ -d "${F_DIR}"/extra/extra ]; then
-  for f in "${F_DIR}"/extra/extra/*.sls; do
+  for f in $(ls "${F_DIR}"/extra/extra/*.sls); do
   echo "    - extra.$(basename ${f} | sed 's/.sls$//g')" >> ${S_DIR}/top.sls
   done
 fi
@@ -376,7 +376,7 @@ if [ -z "${ROLES}" ]; then
     grep -q "letsencrypt" ${S_DIR}/top.sls || echo "    - letsencrypt" >> ${S_DIR}/top.sls
   fi
   echo "    - postgres" >> ${S_DIR}/top.sls
-  echo "    - docker" >> ${S_DIR}/top.sls
+  echo "    - docker.software" >> ${S_DIR}/top.sls
   echo "    - arvados" >> ${S_DIR}/top.sls
 
   # Pillars
@@ -439,7 +439,7 @@ else
       ;;
       "shell")
         # States
-        grep -q "docker" ${S_DIR}/top.sls       || echo "    - docker" >> ${S_DIR}/top.sls
+        grep -q "docker" ${S_DIR}/top.sls       || echo "    - docker.software" >> ${S_DIR}/top.sls
         grep -q "arvados.${R}" ${S_DIR}/top.sls || echo "    - arvados.${R}" >> ${S_DIR}/top.sls
         # Pillars
         grep -q "" ${P_DIR}/top.sls                             || echo "    - docker" >> ${P_DIR}/top.sls
@@ -447,7 +447,7 @@ else
       ;;
       "dispatcher")
         # States
-        grep -q "docker" ${S_DIR}/top.sls       || echo "    - docker" >> ${S_DIR}/top.sls
+        grep -q "docker" ${S_DIR}/top.sls       || echo "    - docker.software" >> ${S_DIR}/top.sls
         grep -q "arvados.${R}" ${S_DIR}/top.sls || echo "    - arvados.${R}" >> ${S_DIR}/top.sls
         # Pillars
         # ATM, no specific pillar needed