Merge branch '18947-keepproxy'
authorTom Clegg <tom@curii.com>
Mon, 11 Apr 2022 15:19:42 +0000 (11:19 -0400)
committerTom Clegg <tom@curii.com>
Mon, 11 Apr 2022 15:19:42 +0000 (11:19 -0400)
refs #18947

Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tom@curii.com>

29 files changed:
apps/workbench/app/controllers/application_controller.rb
apps/workbench/config/arvados_config.rb
build/run-library.sh
build/run-tests.sh
doc/admin/upgrading.html.textile.liquid
doc/user/cwl/cwl-extensions.html.textile.liquid
lib/config/load.go
lib/controller/federation/conn.go
lib/controller/integration_test.go
sdk/R/DESCRIPTION
sdk/python/arvados/util.py
sdk/python/tests/test_util.py
services/api/app/controllers/arvados/v1/links_controller.rb
services/api/app/models/api_client_authorization.rb
services/api/app/models/arvados_model.rb
services/api/db/migrate/20220401153101_fix_created_at_indexes.rb [new file with mode: 0644]
services/api/db/structure.sql
services/api/test/fixtures/jobs.yml
services/api/test/fixtures/pipeline_instances.yml
services/api/test/functional/arvados/v1/query_test.rb
services/api/test/integration/permissions_test.rb
services/api/test/integration/select_test.rb
tools/salt-install/Vagrantfile
tools/salt-install/config_examples/multi_host/aws/pillars/arvados.sls
tools/salt-install/config_examples/multi_host/aws/states/shell_cron_add_login_sync.sls [new file with mode: 0644]
tools/salt-install/config_examples/single_host/multiple_hostnames/pillars/arvados.sls
tools/salt-install/config_examples/single_host/single_hostname/pillars/arvados.sls
tools/salt-install/config_examples/single_host/single_hostname/states/shell_cron_add_login_sync.sls [new file with mode: 0644]
tools/salt-install/provision.sh

index 5312e733f41eb992131752ccf782e112b8c5af2e..f8c8079a1edf4f5878c456d8f4ea0893e2574a32 100644 (file)
@@ -172,7 +172,7 @@ class ApplicationController < ActionController::Base
 
   def find_objects_for_index
     @objects ||= model_class
-    @objects = @objects.filter(@filters).limit(@limit).offset(@offset)
+    @objects = @objects.filter(@filters).limit(@limit).offset(@offset).order(@order)
     @objects.fetch_multiple_pages(false)
   end
 
index c5cc544b9b8717fc70b51c52c1c4b35ce03a16aa..7cc46d2983490896c36e96c9081bf3e74013efa2 100644 (file)
@@ -199,4 +199,8 @@ ArvadosWorkbench::Application.configure do
     ConfigValidators.validate_wb2_url_config()
     ConfigValidators.validate_download_config()
   end
+  if Rails.configuration.Users.AnonymousUserToken and
+     !Rails.configuration.Users.AnonymousUserToken.starts_with?("v2/")
+    Rails.configuration.Users.AnonymousUserToken = "v2/#{clusterID}-gj3su-anonymouspublic/#{Rails.configuration.Users.AnonymousUserToken}"
+  end
 end
index fa2be6ac7a613c88d0083927cc9ad08137af0ffb..47c5e2a39a22f98cc0ad18631e814ea4f4285c63 100755 (executable)
@@ -698,12 +698,15 @@ handle_arvados_src () {
 
 # Usage: handle_libarvados_perl
 handle_libarvados_perl () {
-  if [[ -n "$ONLY_BUILD" ]] || [[ "$ONLY_BUILD" != "libarvados-perl" ]] ; then
+  if [[ -n "$ONLY_BUILD" ]] && [[ "$ONLY_BUILD" != "libarvados-perl" ]] ; then
     debug_echo -e "Skipping build of libarvados-perl package."
     return 0
   fi
-  cd "$WORKSPACE/sdk/perl"
+  # The perl sdk subdirectory is so old that it has no tag in its history,
+  # which causes version_at_commit.sh to fail. Just rebuild it every time.
+  cd "$WORKSPACE"
   libarvados_perl_version="$(version_from_git)"
+  cd "$WORKSPACE/sdk/perl"
 
   cd $WORKSPACE/packages/$TARGET
   test_package_presence libarvados-perl "$libarvados_perl_version"
@@ -721,7 +724,7 @@ handle_libarvados_perl () {
     perl Makefile.PL INSTALL_BASE=install >"$STDOUT_IF_DEBUG" && \
         make install INSTALLDIRS=perl >"$STDOUT_IF_DEBUG" && \
         fpm_build "$WORKSPACE/sdk/perl" install/lib/=/usr/share libarvados-perl \
-        dir "$(version_from_git)" install/man/=/usr/share/man \
+        dir "$libarvados_perl_version" install/man/=/usr/share/man \
         "$WORKSPACE/apache-2.0.txt=/usr/share/doc/libarvados-perl/apache-2.0.txt" && \
         mv --no-clobber libarvados-perl*.$FORMAT "$WORKSPACE/packages/$TARGET/"
   fi
index 3592efbdc2bee5f6c4e25251677c581dcf65c088..67c54c98b0244a72842bccfbb964ce3dbd164c0c 100755 (executable)
@@ -556,6 +556,12 @@ setup_ruby_environment() {
             done
             "$bundle" version | tee /dev/stderr | grep -q 'version 2'
         ) || fatal 'install bundler'
+       if test -d /var/lib/arvados-arvbox/ ; then
+           # Inside arvbox, use bundler-installed binstubs.  The
+           # system bundler and rail's own bin/bundle refuse to work.
+           # I don't know why.
+           bundle=binstubs/bundle
+       fi
     fi
 }
 
index 1ed3b694cee94868535407e0334fa3a982946991..226c8667d67a710283c436db321dc1f26246593d 100644 (file)
@@ -28,12 +28,18 @@ TODO: extract this information based on git commit messages and generate changel
 <div class="releasenotes">
 </notextile>
 
-h2(#main). development main (as of 2022-03-??)
+h2(#main). development main (as of 2022-04-08)
 
-h2(#v2_4_0). v2.4.0 (2022-03-??)
+"previous: Upgrading to 2.4.0":#v2_4_0
+
+h2(#v2_4_0). v2.4.0 (2022-04-08)
 
 "previous: Upgrading to 2.3.1":#v2_3_1
 
+h3. Default result order changed
+
+When requesting a list of objects without an explicit @order@ parameter, the default order has changed from @modified_at desc, uuid asc@ to @modified_at desc, uuid desc@.  This means that if two objects have identical @modified_at@ timestamps, the tiebreaker will now be based on @uuid@ in decending order where previously it would be ascending order. The practical effect of this should be minor; with microsecond precision it is unusual to have two records with exactly the same timestamp, and order-sensitive queries should already provide an explicit @order@ parameter.
+
 h3. Ubuntu 18.04 Arvados Python packages now depend on python-3.8
 
 Ubuntu 18.04 ships with Python 3.6 as the default version of Python 3. Ubuntu also ships a version of Python 3.8, and the Arvados Python packages (@python3-arvados-cwl-runner@, @python3-arvados-fuse@, @python3-arvados-python-client@, @python3-arvados-user-activity@ and @python3-crunchstat-summary@) now depend on the @python-3.8@ system package.
@@ -46,7 +52,7 @@ The minimum supported Ruby version is now 2.6.  If you are running Arvados on De
 
 h3. Anonymous token changes
 
-The anonymous token configured in @Users.AnonymousUserToken@ must now be 32 characters or longer. This was already the suggestion in the documentation, now it is enforced. The @script/get_anonymous_user_token.rb@ script that was needed to register the anonymous user token in the database has been removed. Registration of the anonymous token is no longer necessary. If the anonymous token in @config.yml@ is specified as a full V2 token, that will now generate a warning - it should be updated to list just the secret (i.e. the part after the last forward slash).
+The anonymous token configured in @Users.AnonymousUserToken@ must now be 32 characters or longer. This was already the suggestion in the documentation, now it is enforced. The @script/get_anonymous_user_token.rb@ script that was needed to register the anonymous user token in the database has been removed. Registration of the anonymous token is no longer necessary.
 
 h3. Preemptible instance support changes
 
index d6148d7eee1f3d2992a9d43bcb9c2ca44b3b68b5..0e97e07da3864faa821173536836ef0b6d54a88d 100644 (file)
@@ -60,9 +60,9 @@ hints:
 
   cwltool:CUDARequirement:
     cudaVersionMin: "11.0"
-    cudaComputeCapabilityMin: "9.0"
-    deviceCountMin: 1
-    deviceCountMax: 1
+    cudaComputeCapability: "9.0"
+    cudaDeviceCountMin: 1
+    cudaDeviceCountMax: 1
 
   arv:UsePreemptible:
     usePreemptible: true
@@ -163,9 +163,9 @@ Request support for Nvidia CUDA GPU acceleration in the container.  Assumes that
 table(table table-bordered table-condensed).
 |_. Field |_. Type |_. Description |
 |cudaVersionMin|string|Required.  The CUDA SDK version corresponding to the minimum driver version supported by the container (generally, the SDK version 'X.Y' the application was compiled against).|
-|cudaComputeCapabilityMin|string|Required.  The minimum CUDA hardware capability (in 'X.Y' format) required by the application's PTX or C++ GPU code (will be JIT compiled for the available hardware).|
-|deviceCountMin|integer|Minimum number of GPU devices to allocate on a single node. Required.|
-|deviceCountMax|integer|Maximum number of GPU devices to allocate on a single node. Optional.  If not specified, same as @minDeviceCount@.|
+|cudaComputeCapability|string|Required.  The minimum CUDA hardware capability (in 'X.Y' format) required by the application's PTX or C++ GPU code (will be JIT compiled for the available hardware).|
+|cudaDeviceCountMin|integer|Minimum number of GPU devices to allocate on a single node. Required.|
+|cudaDeviceCountMax|integer|Maximum number of GPU devices to allocate on a single node. Optional.  If not specified, same as @cudaDeviceCountMin@.|
 
 h2(#UsePreemptible). arv:UsePreemptible
 
index de43b9d2e2749b56ef37a100f57e57cd071d59e0..5afb51c5adcfd6dce89d069670b9ce3aadde2a2e 100644 (file)
@@ -369,10 +369,12 @@ func (ldr *Loader) checkToken(label, token string, mandatory bool, acceptV2 bool
                if !strings.HasPrefix(token, "v2/") {
                        return fmt.Errorf("%s: unacceptable characters in token (only a-z, A-Z, 0-9 are acceptable)", label)
                }
-               ldr.Logger.Warnf("%s: token is a full V2 token, should just be a secret (remove everything up to and including the last forward slash)", label)
                if !acceptableTokenRe.MatchString(tmp[2]) {
                        return fmt.Errorf("%s: unacceptable characters in V2 token secret (only a-z, A-Z, 0-9 are acceptable)", label)
                }
+               if len(tmp[2]) < acceptableTokenLength {
+                       ldr.Logger.Warnf("%s: secret is too short (should be at least %d characters)", label, acceptableTokenLength)
+               }
        } else if len(token) < acceptableTokenLength {
                if ldr.Logger != nil {
                        ldr.Logger.Warnf("%s: token is too short (should be at least %d characters)", label, acceptableTokenLength)
index d3819f6262df8f7df4134753d0359e1d04e12950..1b8ec9e64a6e01138a1bfc58a599a78eb2f0e44b 100644 (file)
@@ -69,14 +69,17 @@ func saltedTokenProvider(cluster *arvados.Cluster, local backend, remoteID strin
                        return nil, errors.New("no token provided")
                }
                for _, token := range incoming.Tokens {
-                       if strings.HasPrefix(token, "v2/"+cluster.ClusterID+"-") && remoteID == cluster.Login.LoginCluster {
-                               // If we did this, the login cluster
-                               // would call back to us and then
-                               // reject our response because the
-                               // user UUID prefix (i.e., the
-                               // LoginCluster prefix) won't match
-                               // the token UUID prefix (i.e., our
-                               // prefix).
+                       if strings.HasPrefix(token, "v2/"+cluster.ClusterID+"-") &&
+                               !strings.HasPrefix(token, "v2/"+cluster.ClusterID+"-gj3su-anonymouspublic/") &&
+                               remoteID == cluster.Login.LoginCluster {
+                               // If we did this, the login cluster would call back to us and then
+                               // reject our response because the user UUID prefix (i.e., the
+                               // LoginCluster prefix) won't match the token UUID prefix (i.e., our
+                               // prefix). The anonymous token is OK to forward, because (unlike other
+                               // local tokens for real users) the validation callback will return the
+                               // locally issued anonymous user ID instead of a login-cluster user ID.
+                               // That anonymous user ID gets mapped to the local anonymous user
+                               // automatically on the login cluster.
                                return nil, httpErrorf(http.StatusUnauthorized, "cannot use a locally issued token to forward a request to our login cluster (%s)", remoteID)
                        }
                        salted, err := auth.SaltToken(token, remoteID)
index 9f5d12598b70e31728fe29fb28ae4fa8ce58ec4e..b71c4afb55ba9a5ac9d3fb57efdb050d55c7a34b 100644 (file)
@@ -379,6 +379,56 @@ func (s *IntegrationSuite) TestGetCollectionAsAnonymous(c *check.C) {
        c.Check(coll.PortableDataHash, check.Equals, pdh)
 }
 
+// z3333 should forward the locally-issued anonymous user token to its login
+// cluster z1111. That is no problem because the login cluster controller will
+// map any anonymous user token to its local anonymous user.
+//
+// This needs to work because wb1 has a tendency to slap the local anonymous
+// user token on every request as a reader_token, which gets folded into the
+// request token list controller.
+//
+// Use a z1111 user token and the anonymous token from z3333 passed in as a
+// reader_token to do a request on z3333, asking for the z1111 anonymous user
+// object. The request will be forwarded to the z1111 cluster. The presence of
+// the z3333 anonymous user token should not prohibit the request from being
+// forwarded.
+func (s *IntegrationSuite) TestForwardAnonymousTokenToLoginCluster(c *check.C) {
+       conn1 := s.testClusters["z1111"].Conn()
+       s.testClusters["z3333"].Conn()
+
+       rootctx1, _, _ := s.testClusters["z1111"].RootClients()
+       _, anonac3, _ := s.testClusters["z3333"].AnonymousClients()
+
+       // Make a user connection to z3333 (using a z1111 user, because that's the login cluster)
+       _, userac1, _, _ := s.testClusters["z3333"].UserClients(rootctx1, c, conn1, "user@example.com", true)
+
+       // Get the anonymous user token for z3333
+       var anon3Auth arvados.APIClientAuthorization
+       err := anonac3.RequestAndDecode(&anon3Auth, "GET", "/arvados/v1/api_client_authorizations/current", nil, nil)
+       c.Check(err, check.IsNil)
+
+       var userList arvados.UserList
+       where := make(map[string]string)
+       where["uuid"] = "z1111-tpzed-anonymouspublic"
+       err = userac1.RequestAndDecode(&userList, "GET", "/arvados/v1/users", nil,
+               map[string]interface{}{
+                       "reader_tokens": []string{anon3Auth.TokenV2()},
+                       "where":         where,
+               },
+       )
+       // The local z3333 anonymous token must be allowed to be forwarded to the login cluster
+       c.Check(err, check.IsNil)
+
+       userac1.AuthToken = "v2/z1111-gj3su-asdfasdfasdfasd/this-token-does-not-validate-so-anonymous-token-will-be-used-instead"
+       err = userac1.RequestAndDecode(&userList, "GET", "/arvados/v1/users", nil,
+               map[string]interface{}{
+                       "reader_tokens": []string{anon3Auth.TokenV2()},
+                       "where":         where,
+               },
+       )
+       c.Check(err, check.IsNil)
+}
+
 // Get a token from the login cluster (z1111), use it to submit a
 // container request on z2222.
 func (s *IntegrationSuite) TestCreateContainerRequestWithFedToken(c *check.C) {
index 75ac892b4ba6680a7af4c91e47f1e770fcd7020c..9c682412a04c6ae9d21448f2aa19742b791100e2 100644 (file)
@@ -1,7 +1,7 @@
 Package: ArvadosR
 Type: Package
 Title: Arvados R SDK
-Version: 0.0.6
+Version: 2.4.0
 Authors@R: c(person("Fuad", "Muhic", role = c("aut", "ctr"), email = "fmuhic@capeannenterprises.com"),
              person("Peter", "Amstutz", role = c("cre"), email = "peter.amstutz@curii.com"))
 Description: This is the Arvados R SDK
index be8a03fc314d2cf599c16d5f44b1ab61cc9e885d..c383d529e8087da579fcf4ae6814f76a57044e29 100644 (file)
@@ -392,7 +392,8 @@ def keyset_list_all(fn, order_key="created_at", num_retries=0, ascending=True, *
     pagesize = 1000
     kwargs["limit"] = pagesize
     kwargs["count"] = 'none'
-    kwargs["order"] = ["%s %s" % (order_key, "asc" if ascending else "desc"), "uuid asc"]
+    asc = "asc" if ascending else "desc"
+    kwargs["order"] = ["%s %s" % (order_key, asc), "uuid %s" % asc]
     other_filters = kwargs.get("filters", [])
 
     if "select" in kwargs and "uuid" not in kwargs["select"]:
@@ -436,7 +437,7 @@ def keyset_list_all(fn, order_key="created_at", num_retries=0, ascending=True, *
         if firstitem[order_key] == lastitem[order_key]:
             # Got a page where every item has the same order key.
             # Switch to using uuid for paging.
-            nextpage = [[order_key, "=", lastitem[order_key]], ["uuid", ">", lastitem["uuid"]]]
+            nextpage = [[order_key, "=", lastitem[order_key]], ["uuid", ">" if ascending else "<", lastitem["uuid"]]]
             prev_page_all_same_order_key = True
         else:
             # Start from the last order key seen, but skip the last
index 1c0e437b41196bf1e56e2048e11029725b01da2e..4dba9ce3dc7a5105533a526fe3ee304ed60d784c 100644 (file)
@@ -166,10 +166,10 @@ class KeysetListAllTestCase(unittest.TestCase):
 
     def test_onepage_desc(self):
         ks = KeysetTestHelper([[
-            {"limit": 1000, "count": "none", "order": ["created_at desc", "uuid asc"], "filters": []},
+            {"limit": 1000, "count": "none", "order": ["created_at desc", "uuid desc"], "filters": []},
             {"items": [{"created_at": "2", "uuid": "2"}, {"created_at": "1", "uuid": "1"}]}
         ], [
-            {"limit": 1000, "count": "none", "order": ["created_at desc", "uuid asc"], "filters": [["created_at", "<=", "1"], ["uuid", "!=", "1"]]},
+            {"limit": 1000, "count": "none", "order": ["created_at desc", "uuid desc"], "filters": [["created_at", "<=", "1"], ["uuid", "!=", "1"]]},
             {"items": []}
         ]])
 
index 384ffd64b7080190c21a4ec46f2ab7ea31d97c83..7716a3d5cffd9f6ac44d3bb691d06c98ddf7acf2 100644 (file)
@@ -26,8 +26,8 @@ class Arvados::V1::LinksController < ApplicationController
   def get_permissions
     if current_user.andand.can?(manage: @object)
       # find all links and return them
-      @objects = Link.where(link_class: "permission",
-                            head_uuid: params[:uuid])
+      @objects = Link.unscoped.where(link_class: "permission",
+                                     head_uuid: params[:uuid])
       @offset = 0
       @limit = @objects.count
       render_list
@@ -39,14 +39,37 @@ class Arvados::V1::LinksController < ApplicationController
   protected
 
   def find_object_by_uuid
+    if params[:id] && params[:id].match(/\D/)
+      params[:uuid] = params.delete :id
+    end
     if action_name == 'get_permissions'
       # get_permissions accepts a UUID for any kind of object.
       @object = ArvadosModel::resource_class_for_uuid(params[:uuid])
         .readable_by(*@read_users)
         .where(uuid: params[:uuid])
         .first
-    else
+    elsif !current_user
       super
+    else
+      # The usual permission-filtering index query is unnecessarily
+      # inefficient, and doesn't match all permission links that
+      # should be visible (see #18865).  Instead, we look up the link
+      # by UUID, then check whether (a) its tail_uuid is the current
+      # user or (b) its head_uuid is an object the current_user
+      # can_manage.
+      link = Link.unscoped.where(uuid: params[:uuid]).first
+      if link && link.link_class != 'permission'
+        # Not a permission link. Re-fetch using generic
+        # permission-filtering query.
+        super
+      elsif link && (current_user.uuid == link.tail_uuid ||
+                     current_user.can?(manage: link.head_uuid))
+        # Permission granted.
+        @object = link
+      else
+        # Permission denied, i.e., link is invisible => 404.
+        @object = nil
+      end
     end
   end
 
@@ -86,6 +109,32 @@ class Arvados::V1::LinksController < ApplicationController
         k
       end
     end
+
+    # If the provided filters are enough to limit the results to
+    # permission links with specific head_uuids or
+    # tail_uuid=current_user, bypass the normal readable_by query
+    # (which doesn't match all can_manage-able items, see #18865) --
+    # just ensure the current user actually has can_manage permission
+    # for the provided head_uuids, removing any that don't. At that
+    # point the caller's filters are an effective permission filter.
+    if @filters.include?(['link_class', '=', 'permission'])
+      @filters.map do |k|
+        if k[0] == 'tail_uuid' && k[1] == '=' && k[2] == current_user.uuid
+          @objects = Link.unscoped
+        elsif k[0] == 'head_uuid'
+          if k[1] == '=' && current_user.can?(manage: k[2])
+            @objects = Link.unscoped
+          elsif k[1] == 'in'
+            # Modify the filter operand element (k[2]) in place,
+            # removing any non-permitted UUIDs.
+            k[2].select! do |head_uuid|
+              current_user.can?(manage: head_uuid)
+            end
+            @objects = Link.unscoped
+          end
+        end
+      end
+    end
   end
 
 end
index 993a49e5b75e7ecfb782a306df16c74b37fbed4a..52922d32b1868fdb53d8bcd3f1197d149a93bb63 100644 (file)
@@ -116,7 +116,7 @@ class ApiClientAuthorization < ArvadosModel
     clnt
   end
 
-  def self.check_anonymous_user_token token
+  def self.check_anonymous_user_token(token:, remote:)
     case token[0..2]
     when 'v2/'
       _, token_uuid, secret, optional = token.split('/')
@@ -130,11 +130,16 @@ class ApiClientAuthorization < ArvadosModel
       secret = token
     end
 
+    # Usually, the secret is salted
+    salted_secret = OpenSSL::HMAC.hexdigest('sha1', Rails.configuration.Users.AnonymousUserToken, remote)
+
+    # The anonymous token could be specified as a full v2 token in the config,
+    # but the config loader strips it down to the secret part.
     # The anonymous token content and minimum length is verified in lib/config
-    if secret.length >= 0 && secret == Rails.configuration.Users.AnonymousUserToken
+    if secret.length >= 0 && (secret == Rails.configuration.Users.AnonymousUserToken || secret == salted_secret)
       return ApiClientAuthorization.new(user: User.find_by_uuid(anonymous_user_uuid),
                                         uuid: Rails.configuration.ClusterID+"-gj3su-anonymouspublic",
-                                        api_token: token,
+                                        api_token: secret,
                                         api_client: anonymous_user_token_api_client,
                                         scopes: ['GET /'])
     else
@@ -157,7 +162,7 @@ class ApiClientAuthorization < ArvadosModel
     return nil if token.nil? or token.empty?
     remote ||= Rails.configuration.ClusterID
 
-    auth = self.check_anonymous_user_token(token)
+    auth = self.check_anonymous_user_token(token: token, remote: remote)
     if !auth.nil?
       return auth
     end
index 327bf63b5fa057779d6d03d99331b179077611db..07a31d81a8a129dc67acb4aa1a7fa8f39e253ea1 100644 (file)
@@ -220,7 +220,7 @@ class ArvadosModel < ApplicationRecord
   end
 
   def self.default_orders
-    ["#{table_name}.modified_at desc", "#{table_name}.uuid"]
+    ["#{table_name}.modified_at desc", "#{table_name}.uuid desc"]
   end
 
   def self.unique_columns
diff --git a/services/api/db/migrate/20220401153101_fix_created_at_indexes.rb b/services/api/db/migrate/20220401153101_fix_created_at_indexes.rb
new file mode 100644 (file)
index 0000000..590e841
--- /dev/null
@@ -0,0 +1,28 @@
+# Copyright (C) The Arvados Authors. All rights reserved.
+#
+# SPDX-License-Identifier: AGPL-3.0
+
+class FixCreatedAtIndexes < ActiveRecord::Migration[5.2]
+  @@idxtables = [:collections, :container_requests, :groups, :links, :repositories, :users, :virtual_machines, :workflows, :logs]
+
+  def up
+    @@idxtables.each do |table|
+      ActiveRecord::Base.connection.execute("DROP INDEX IF EXISTS index_#{table.to_s}_on_created_at")
+      ActiveRecord::Base.connection.execute("DROP INDEX IF EXISTS index_#{table.to_s}_on_created_at_uuid")
+      ActiveRecord::Base.connection.execute("DROP INDEX IF EXISTS index_#{table.to_s}_on_created_at_and_uuid")
+      ActiveRecord::Base.connection.execute("DROP INDEX IF EXISTS index_#{table.to_s}_on_modified_at")
+      ActiveRecord::Base.connection.execute("DROP INDEX IF EXISTS index_#{table.to_s}_on_modified_at_uuid")
+      ActiveRecord::Base.connection.execute("DROP INDEX IF EXISTS index_#{table.to_s}_on_modified_at_and_uuid")
+
+      ActiveRecord::Base.connection.execute("CREATE INDEX IF NOT EXISTS index_#{table.to_s}_on_created_at_and_uuid ON #{table.to_s} USING btree (created_at, uuid)")
+      ActiveRecord::Base.connection.execute("CREATE INDEX IF NOT EXISTS index_#{table.to_s}_on_modified_at_and_uuid ON #{table.to_s} USING btree (modified_at, uuid)")
+    end
+  end
+
+  def down
+    @@idxtables.each do |table|
+      ActiveRecord::Base.connection.execute("DROP INDEX IF EXISTS index_#{table.to_s}_on_modified_at_and_uuid")
+      ActiveRecord::Base.connection.execute("CREATE INDEX IF NOT EXISTS index_#{table.to_s}_on_modified_at_uuid ON #{table.to_s} USING btree (modified_at desc, uuid asc)")
+    end
+  end
+end
index cfe21f7c9ae29307b42fd25236a1f4c195254da0..e6bba676257118d542333120a5815549d2cb1f64 100644 (file)
@@ -1905,10 +1905,10 @@ CREATE UNIQUE INDEX index_authorized_keys_on_uuid ON public.authorized_keys USIN
 
 
 --
--- Name: index_collections_on_created_at; Type: INDEX; Schema: public; Owner: -
+-- Name: index_collections_on_created_at_and_uuid; Type: INDEX; Schema: public; Owner: -
 --
 
-CREATE INDEX index_collections_on_created_at ON public.collections USING btree (created_at);
+CREATE INDEX index_collections_on_created_at_and_uuid ON public.collections USING btree (created_at, uuid);
 
 
 --
@@ -1933,17 +1933,10 @@ CREATE INDEX index_collections_on_is_trashed ON public.collections USING btree (
 
 
 --
--- Name: index_collections_on_modified_at; Type: INDEX; Schema: public; Owner: -
+-- Name: index_collections_on_modified_at_and_uuid; Type: INDEX; Schema: public; Owner: -
 --
 
-CREATE INDEX index_collections_on_modified_at ON public.collections USING btree (modified_at);
-
-
---
--- Name: index_collections_on_modified_at_uuid; Type: INDEX; Schema: public; Owner: -
---
-
-CREATE INDEX index_collections_on_modified_at_uuid ON public.collections USING btree (modified_at DESC, uuid);
+CREATE INDEX index_collections_on_modified_at_and_uuid ON public.collections USING btree (modified_at, uuid);
 
 
 --
@@ -1989,10 +1982,17 @@ CREATE INDEX index_container_requests_on_container_uuid ON public.container_requ
 
 
 --
--- Name: index_container_requests_on_modified_at_uuid; Type: INDEX; Schema: public; Owner: -
+-- Name: index_container_requests_on_created_at_and_uuid; Type: INDEX; Schema: public; Owner: -
+--
+
+CREATE INDEX index_container_requests_on_created_at_and_uuid ON public.container_requests USING btree (created_at, uuid);
+
+
+--
+-- Name: index_container_requests_on_modified_at_and_uuid; Type: INDEX; Schema: public; Owner: -
 --
 
-CREATE INDEX index_container_requests_on_modified_at_uuid ON public.container_requests USING btree (modified_at DESC, uuid);
+CREATE INDEX index_container_requests_on_modified_at_and_uuid ON public.container_requests USING btree (modified_at, uuid);
 
 
 --
@@ -2094,10 +2094,10 @@ CREATE UNIQUE INDEX index_frozen_groups_on_uuid ON public.frozen_groups USING bt
 
 
 --
--- Name: index_groups_on_created_at; Type: INDEX; Schema: public; Owner: -
+-- Name: index_groups_on_created_at_and_uuid; Type: INDEX; Schema: public; Owner: -
 --
 
-CREATE INDEX index_groups_on_created_at ON public.groups USING btree (created_at);
+CREATE INDEX index_groups_on_created_at_and_uuid ON public.groups USING btree (created_at, uuid);
 
 
 --
@@ -2122,17 +2122,10 @@ CREATE INDEX index_groups_on_is_trashed ON public.groups USING btree (is_trashed
 
 
 --
--- Name: index_groups_on_modified_at; Type: INDEX; Schema: public; Owner: -
+-- Name: index_groups_on_modified_at_and_uuid; Type: INDEX; Schema: public; Owner: -
 --
 
-CREATE INDEX index_groups_on_modified_at ON public.groups USING btree (modified_at);
-
-
---
--- Name: index_groups_on_modified_at_uuid; Type: INDEX; Schema: public; Owner: -
---
-
-CREATE INDEX index_groups_on_modified_at_uuid ON public.groups USING btree (modified_at DESC, uuid);
+CREATE INDEX index_groups_on_modified_at_and_uuid ON public.groups USING btree (modified_at, uuid);
 
 
 --
@@ -2360,10 +2353,10 @@ CREATE UNIQUE INDEX index_keep_services_on_uuid ON public.keep_services USING bt
 
 
 --
--- Name: index_links_on_created_at; Type: INDEX; Schema: public; Owner: -
+-- Name: index_links_on_created_at_and_uuid; Type: INDEX; Schema: public; Owner: -
 --
 
-CREATE INDEX index_links_on_created_at ON public.links USING btree (created_at);
+CREATE INDEX index_links_on_created_at_and_uuid ON public.links USING btree (created_at, uuid);
 
 
 --
@@ -2374,17 +2367,10 @@ CREATE INDEX index_links_on_head_uuid ON public.links USING btree (head_uuid);
 
 
 --
--- Name: index_links_on_modified_at; Type: INDEX; Schema: public; Owner: -
+-- Name: index_links_on_modified_at_and_uuid; Type: INDEX; Schema: public; Owner: -
 --
 
-CREATE INDEX index_links_on_modified_at ON public.links USING btree (modified_at);
-
-
---
--- Name: index_links_on_modified_at_uuid; Type: INDEX; Schema: public; Owner: -
---
-
-CREATE INDEX index_links_on_modified_at_uuid ON public.links USING btree (modified_at DESC, uuid);
+CREATE INDEX index_links_on_modified_at_and_uuid ON public.links USING btree (modified_at, uuid);
 
 
 --
@@ -2423,10 +2409,10 @@ CREATE UNIQUE INDEX index_links_on_uuid ON public.links USING btree (uuid);
 
 
 --
--- Name: index_logs_on_created_at; Type: INDEX; Schema: public; Owner: -
+-- Name: index_logs_on_created_at_and_uuid; Type: INDEX; Schema: public; Owner: -
 --
 
-CREATE INDEX index_logs_on_created_at ON public.logs USING btree (created_at);
+CREATE INDEX index_logs_on_created_at_and_uuid ON public.logs USING btree (created_at, uuid);
 
 
 --
@@ -2444,17 +2430,10 @@ CREATE INDEX index_logs_on_event_type ON public.logs USING btree (event_type);
 
 
 --
--- Name: index_logs_on_modified_at; Type: INDEX; Schema: public; Owner: -
+-- Name: index_logs_on_modified_at_and_uuid; Type: INDEX; Schema: public; Owner: -
 --
 
-CREATE INDEX index_logs_on_modified_at ON public.logs USING btree (modified_at);
-
-
---
--- Name: index_logs_on_modified_at_uuid; Type: INDEX; Schema: public; Owner: -
---
-
-CREATE INDEX index_logs_on_modified_at_uuid ON public.logs USING btree (modified_at DESC, uuid);
+CREATE INDEX index_logs_on_modified_at_and_uuid ON public.logs USING btree (modified_at, uuid);
 
 
 --
@@ -2605,10 +2584,17 @@ CREATE UNIQUE INDEX index_pipeline_templates_on_uuid ON public.pipeline_template
 
 
 --
--- Name: index_repositories_on_modified_at_uuid; Type: INDEX; Schema: public; Owner: -
+-- Name: index_repositories_on_created_at_and_uuid; Type: INDEX; Schema: public; Owner: -
 --
 
-CREATE INDEX index_repositories_on_modified_at_uuid ON public.repositories USING btree (modified_at DESC, uuid);
+CREATE INDEX index_repositories_on_created_at_and_uuid ON public.repositories USING btree (created_at, uuid);
+
+
+--
+-- Name: index_repositories_on_modified_at_and_uuid; Type: INDEX; Schema: public; Owner: -
+--
+
+CREATE INDEX index_repositories_on_modified_at_and_uuid ON public.repositories USING btree (modified_at, uuid);
 
 
 --
@@ -2689,10 +2675,10 @@ CREATE UNIQUE INDEX index_trashed_groups_on_group_uuid ON public.trashed_groups
 
 
 --
--- Name: index_users_on_created_at; Type: INDEX; Schema: public; Owner: -
+-- Name: index_users_on_created_at_and_uuid; Type: INDEX; Schema: public; Owner: -
 --
 
-CREATE INDEX index_users_on_created_at ON public.users USING btree (created_at);
+CREATE INDEX index_users_on_created_at_and_uuid ON public.users USING btree (created_at, uuid);
 
 
 --
@@ -2703,17 +2689,10 @@ CREATE UNIQUE INDEX index_users_on_identity_url ON public.users USING btree (ide
 
 
 --
--- Name: index_users_on_modified_at; Type: INDEX; Schema: public; Owner: -
---
-
-CREATE INDEX index_users_on_modified_at ON public.users USING btree (modified_at);
-
-
---
--- Name: index_users_on_modified_at_uuid; Type: INDEX; Schema: public; Owner: -
+-- Name: index_users_on_modified_at_and_uuid; Type: INDEX; Schema: public; Owner: -
 --
 
-CREATE INDEX index_users_on_modified_at_uuid ON public.users USING btree (modified_at DESC, uuid);
+CREATE INDEX index_users_on_modified_at_and_uuid ON public.users USING btree (modified_at, uuid);
 
 
 --
@@ -2737,6 +2716,13 @@ CREATE UNIQUE INDEX index_users_on_username ON public.users USING btree (usernam
 CREATE UNIQUE INDEX index_users_on_uuid ON public.users USING btree (uuid);
 
 
+--
+-- Name: index_virtual_machines_on_created_at_and_uuid; Type: INDEX; Schema: public; Owner: -
+--
+
+CREATE INDEX index_virtual_machines_on_created_at_and_uuid ON public.virtual_machines USING btree (created_at, uuid);
+
+
 --
 -- Name: index_virtual_machines_on_hostname; Type: INDEX; Schema: public; Owner: -
 --
@@ -2745,10 +2731,10 @@ CREATE INDEX index_virtual_machines_on_hostname ON public.virtual_machines USING
 
 
 --
--- Name: index_virtual_machines_on_modified_at_uuid; Type: INDEX; Schema: public; Owner: -
+-- Name: index_virtual_machines_on_modified_at_and_uuid; Type: INDEX; Schema: public; Owner: -
 --
 
-CREATE INDEX index_virtual_machines_on_modified_at_uuid ON public.virtual_machines USING btree (modified_at DESC, uuid);
+CREATE INDEX index_virtual_machines_on_modified_at_and_uuid ON public.virtual_machines USING btree (modified_at, uuid);
 
 
 --
@@ -2766,10 +2752,17 @@ CREATE UNIQUE INDEX index_virtual_machines_on_uuid ON public.virtual_machines US
 
 
 --
--- Name: index_workflows_on_modified_at_uuid; Type: INDEX; Schema: public; Owner: -
+-- Name: index_workflows_on_created_at_and_uuid; Type: INDEX; Schema: public; Owner: -
+--
+
+CREATE INDEX index_workflows_on_created_at_and_uuid ON public.workflows USING btree (created_at, uuid);
+
+
+--
+-- Name: index_workflows_on_modified_at_and_uuid; Type: INDEX; Schema: public; Owner: -
 --
 
-CREATE INDEX index_workflows_on_modified_at_uuid ON public.workflows USING btree (modified_at DESC, uuid);
+CREATE INDEX index_workflows_on_modified_at_and_uuid ON public.workflows USING btree (modified_at, uuid);
 
 
 --
@@ -3185,6 +3178,7 @@ INSERT INTO "schema_migrations" (version) VALUES
 ('20211027154300'),
 ('20220224203102'),
 ('20220301155729'),
-('20220303204419');
+('20220303204419'),
+('20220401153101');
 
 
index ab76417902214162506707d3e642f93539ffe7ed..9280aeab935e19748f25ea347592d3d704161f02 100644 (file)
@@ -8,8 +8,8 @@ running:
   cancelled_at: ~
   cancelled_by_user_uuid: ~
   cancelled_by_client_uuid: ~
-  created_at: <%= 3.minute.ago.to_s(:db) %>
-  started_at: <%= 3.minute.ago.to_s(:db) %>
+  created_at: <%= 2.7.minute.ago.to_s(:db) %>
+  started_at: <%= 2.7.minute.ago.to_s(:db) %>
   finished_at: ~
   script: hash
   repository: active/foo
index 9621b3effc1c74f0b832c021b3c9d2b99ef11586..a504c9fadd790cb21e2410cb95cefd082c32cfbf 100644 (file)
@@ -97,7 +97,7 @@ has_job:
   state: Ready
   uuid: zzzzz-d1hrv-1yfj6xkidf2muk3
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
-  created_at: <%= 3.1.minute.ago.to_s(:db) %>
+  created_at: <%= 2.9.minute.ago.to_s(:db) %>
   components:
    foo:
     script: foo
@@ -112,7 +112,7 @@ components_is_jobspec:
   # Helps test that clients cope with funny-shaped components.
   # For an example, see #3321.
   uuid: zzzzz-d1hrv-1yfj61234abcdk4
-  created_at: <%= 2.minute.ago.to_s(:db) %>
+  created_at: <%= 4.minute.ago.to_s(:db) %>
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   modified_by_client_uuid: zzzzz-ozdt8-brczlopd8u8d0jr
   modified_by_user_uuid: zzzzz-tpzed-xurymjxw79nv3jz
@@ -132,7 +132,7 @@ pipeline_with_tagged_collection_input:
   state: Ready
   uuid: zzzzz-d1hrv-1yfj61234abcdk3
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
-  created_at: <%= 3.1.minute.ago.to_s(:db) %>
+  created_at: <%= 3.2.minute.ago.to_s(:db) %>
   components:
     part-one:
       script_parameters:
@@ -145,7 +145,7 @@ pipeline_to_merge_params:
   uuid: zzzzz-d1hrv-1yfj6dcba4321k3
   pipeline_template_uuid: zzzzz-p5p6p-aox0k0ofxrystgw
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
-  created_at: <%= 3.1.minute.ago.to_s(:db) %>
+  created_at: <%= 3.3.minute.ago.to_s(:db) %>
   components:
     part-one:
       script_parameters:
@@ -193,7 +193,7 @@ pipeline_instance_owned_by_fuse:
   uuid: zzzzz-d1hrv-ri9dvgkgqs9y09j
   owner_uuid: zzzzz-tpzed-0fusedrivertest
   pipeline_template_uuid: zzzzz-p5p6p-vq4wuvy84xvaq2r
-  created_at: 2014-09-15 12:00:00
+  created_at: 2014-09-16 12:00:00
   name: "pipeline instance owned by FUSE"
   components:
     foo:
@@ -210,7 +210,7 @@ pipeline_instance_in_fuse_project:
   uuid: zzzzz-d1hrv-scarxiyajtshq3l
   owner_uuid: zzzzz-j7d0g-0000ownedbyfuse
   pipeline_template_uuid: zzzzz-p5p6p-vq4wuvy84xvaq2r
-  created_at: 2014-09-15 12:00:00
+  created_at: 2014-09-17 12:00:00
   name: "pipeline instance in FUSE project"
   components:
     foo:
@@ -227,7 +227,7 @@ pipeline_owned_by_active_in_aproject:
   state: Complete
   uuid: zzzzz-d1hrv-ju5ghi0i9z2kqc6
   owner_uuid: zzzzz-j7d0g-v955i6s2oi1cbso
-  created_at: 2014-09-15 12:00:00
+  created_at: 2014-09-18 12:00:00
   components:
     foo:
       script: foo
@@ -243,7 +243,7 @@ pipeline_owned_by_active_in_home:
   state: Complete
   uuid: zzzzz-d1hrv-lihrbd0i9z2kqc6
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
-  created_at: 2014-09-15 12:00:00
+  created_at: 2014-09-19 12:00:00
   components:
     foo:
       script: foo
@@ -287,7 +287,7 @@ pipeline_in_publicly_accessible_project_but_other_objects_elsewhere:
   name: Pipeline in public project with other objects elsewhere
   pipeline_template_uuid: zzzzz-p5p6p-aox0k0ofxrystgw
   state: Complete
-  created_at: 2014-09-15 12:00:00
+  created_at: 2014-09-20 12:00:00
   components:
     foo:
       script: foo
@@ -314,7 +314,7 @@ new_pipeline_in_publicly_accessible_project:
   name: Pipeline in New state in publicly accessible project
   pipeline_template_uuid: zzzzz-p5p6p-tmpltpublicproj
   state: New
-  created_at: 2014-09-15 12:00:00
+  created_at: 2014-09-21 12:00:00
   components:
     foo:
       script: foo
@@ -331,7 +331,7 @@ new_pipeline_in_publicly_accessible_project_but_other_objects_elsewhere:
   name: Pipeline in New state in public project with objects elsewhere
   pipeline_template_uuid: zzzzz-p5p6p-aox0k0ofxrystgw
   state: New
-  created_at: 2014-09-15 12:00:00
+  created_at: 2014-09-22 12:00:00
   components:
     foo:
       script: foo
@@ -348,7 +348,7 @@ new_pipeline_in_publicly_accessible_project_with_dataclass_file_and_other_object
   name: Pipeline in public project in New state with file type data class with objects elsewhere
   pipeline_template_uuid: zzzzz-p5p6p-aox0k0ofxrystgw
   state: New
-  created_at: 2014-09-15 12:00:00
+  created_at: 2014-09-23 12:00:00
   components:
     foo:
       script: foo
@@ -363,8 +363,8 @@ pipeline_in_running_state:
   name: running_with_job
   uuid: zzzzz-d1hrv-runningpipeline
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
-  created_at: <%= 3.1.minute.ago.to_s(:db) %>
-  started_at: <%= 3.1.minute.ago.to_s(:db) %>
+  created_at: <%= 2.8.minute.ago.to_s(:db) %>
+  started_at: <%= 2.8.minute.ago.to_s(:db) %>
   state: RunningOnServer
   components:
    foo:
@@ -393,7 +393,7 @@ complete_pipeline_with_two_jobs:
   uuid: zzzzz-d1hrv-twodonepipeline
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   state: Complete
-  created_at: <%= 3.minute.ago.to_s(:db) %>
+  created_at: <%= 2.5.minute.ago.to_s(:db) %>
   started_at: <%= 2.minute.ago.to_s(:db) %>
   finished_at: <%= 1.minute.ago.to_s(:db) %>
   components:
index 9bba418578e89c6dd36009ba2e3278f700d33eb3..fae9dc40c6daf2c9df1e28edae47cd743757db52 100644 (file)
@@ -24,7 +24,7 @@ class Arvados::V1::QueryTest < ActionController::TestCase
       controller: 'logs',
     }
     assert_response :success
-    assert_equal('logs.event_type asc, logs.modified_at desc, logs.uuid',
+    assert_equal('logs.event_type asc, logs.modified_at desc, logs.uuid desc',
                  assigns(:objects).order_values.join(', '))
   end
 
@@ -36,7 +36,7 @@ class Arvados::V1::QueryTest < ActionController::TestCase
       controller: 'logs',
     }
     assert_response :success
-    assert_equal('logs.modified_at asc, logs.uuid',
+    assert_equal('logs.modified_at asc, logs.uuid desc',
                  assigns(:objects).order_values.join(', '))
   end
 
@@ -51,7 +51,7 @@ class Arvados::V1::QueryTest < ActionController::TestCase
       controller: 'logs',
     }
     assert_response :success
-    assert_equal('logs.modified_at asc, logs.event_type desc, logs.uuid',
+    assert_equal('logs.modified_at asc, logs.event_type desc, logs.uuid desc',
                  assigns(:objects).order_values.join(', '))
   end
 
index 9eae518c1d0f7ffd0700fc110de375754713fdc1..65f5adc1d150b0914324ebde3357187214c71bc2 100644 (file)
@@ -451,35 +451,226 @@ class PermissionsTest < ActionDispatch::IntegrationTest
 
     # Should be able to read links directly too
     get "/arvados/v1/links/#{can_read_uuid}",
-        params: {},
       headers: auth(:active)
     assert_response :success
 
+    ### Create some objects of different types (other than projects)
+    ### inside a subproject inside the shared project, and share those
+    ### individual objects with a 3rd user ("spectator").
+    post '/arvados/v1/groups',
+         params: {
+           group: {
+             owner_uuid: groups(:public).uuid,
+             name: 'permission test subproject',
+             group_class: 'project',
+           },
+         },
+         headers: auth(:admin)
+    assert_response :success
+    subproject_uuid = json_response['uuid']
+
+    test_types = ['collection', 'workflow', 'container_request']
+    test_type_create_attrs = {
+      'container_request' => {
+        command: ["echo", "foo"],
+        container_image: links(:docker_image_collection_tag).name,
+        cwd: "/tmp",
+        environment: {},
+        mounts: {"/out" => {kind: "tmp", capacity: 1000000}},
+        output_path: "/out",
+        runtime_constraints: {"vcpus" => 1, "ram" => 2},
+      },
+    }
+
+    test_object = {}
+    test_object_perm_link = {}
+    test_types.each do |test_type|
+      post "/arvados/v1/#{test_type}s",
+           params: {
+             test_type.to_sym => {
+               owner_uuid: subproject_uuid,
+               name: "permission test #{test_type} in subproject",
+             }.merge(test_type_create_attrs[test_type] || {}).to_json,
+           },
+           headers: auth(:admin)
+      assert_response :success
+      test_object[test_type] = json_response
+
+      post '/arvados/v1/links',
+           params: {
+             link: {
+               tail_uuid: users(:spectator).uuid,
+               link_class: 'permission',
+               name: 'can_read',
+               head_uuid: test_object[test_type]['uuid'],
+             }
+           },
+           headers: auth(:admin)
+      assert_response :success
+      test_object_perm_link[test_type] = json_response
+    end
+
+    # The "active-can_manage-project" permission should cause the
+    # "spectator-can_read-object" links to be visible to the "active"
+    # user.
+    test_types.each do |test_type|
+      get "/arvados/v1/permissions/#{test_object[test_type]['uuid']}",
+          headers: auth(:active)
+      assert_response :success
+      perm_uuids = json_response['items'].map { |item| item['uuid'] }
+      assert_includes perm_uuids, test_object_perm_link[test_type]['uuid'], "can_read_uuid not found"
+
+      get "/arvados/v1/links/#{test_object_perm_link[test_type]['uuid']}",
+          headers: auth(:active)
+      assert_response :success
+
+      [
+        ['head_uuid', '=', test_object[test_type]['uuid']],
+        ['head_uuid', 'in', [test_object[test_type]['uuid']]],
+        ['head_uuid', 'in', [users(:admin).uuid, test_object[test_type]['uuid']]],
+      ].each do |filter|
+        get "/arvados/v1/links",
+            params: {
+              filters: ([['link_class', '=', 'permission'], filter]).to_json,
+            },
+            headers: auth(:active)
+        assert_response :success
+        assert_not_empty json_response['items'], "could not find can_read link using index with filter #{filter}"
+        assert_equal test_object_perm_link[test_type]['uuid'], json_response['items'][0]['uuid']
+      end
+
+      # The "spectator-can_read-object" link should be visible to the
+      # subject user ("spectator") in a filter query, even without
+      # can_manage permission on the target object.
+      [
+        ['tail_uuid', '=', users(:spectator).uuid],
+      ].each do |filter|
+        get "/arvados/v1/links",
+            params: {
+              filters: ([['link_class', '=', 'permission'], filter]).to_json,
+            },
+            headers: auth(:spectator)
+        assert_response :success
+        perm_uuids = json_response['items'].map { |item| item['uuid'] }
+        assert_includes perm_uuids, test_object_perm_link[test_type]['uuid'], "could not find can_read link using index with filter #{filter}"
+      end
+    end
+
     ### Now delete the can_manage link
     delete "/arvados/v1/links/#{can_manage_uuid}",
-      params: nil,
       headers: auth(:active)
     assert_response :success
 
     # Should not be able read these permission links again
-    get "/arvados/v1/permissions/#{groups(:public).uuid}",
-      params: nil,
+    test_types.each do |test_type|
+      get "/arvados/v1/permissions/#{groups(:public).uuid}",
+          headers: auth(:active)
+      assert_response 404
+
+      get "/arvados/v1/permissions/#{test_object[test_type]['uuid']}",
+          headers: auth(:active)
+      assert_response 404
+
+      get "/arvados/v1/links",
+          params: {
+            filters: [["link_class", "=", "permission"], ["head_uuid", "=", groups(:public).uuid]].to_json
+          },
+          headers: auth(:active)
+      assert_response :success
+      assert_equal [], json_response['items']
+
+      [
+        ['head_uuid', '=', test_object[test_type]['uuid']],
+        ['head_uuid', 'in', [users(:admin).uuid, test_object[test_type]['uuid']]],
+        ['head_uuid', 'in', []],
+      ].each do |filter|
+        get "/arvados/v1/links",
+            params: {
+              :filters => [["link_class", "=", "permission"], filter].to_json
+            },
+            headers: auth(:active)
+        assert_response :success
+        assert_equal [], json_response['items']
+      end
+
+      # Should not be able to read links directly either
+      get "/arvados/v1/links/#{can_read_uuid}",
+          headers: auth(:active)
+      assert_response 404
+
+      test_types.each do |test_type|
+        get "/arvados/v1/links/#{test_object_perm_link[test_type]['uuid']}",
+            headers: auth(:active)
+        assert_response 404
+      end
+    end
+
+    ### Create a collection, and share it with a direct permission
+    ### link (as opposed to sharing its parent project)
+    post "/arvados/v1/collections",
+      params: {
+        collection: {
+          name: 'permission test',
+        }
+      },
+      headers: auth(:admin)
+    assert_response :success
+    collection_uuid = json_response['uuid']
+    post "/arvados/v1/links",
+      params: {
+        link: {
+          tail_uuid: users(:spectator).uuid,
+          link_class: 'permission',
+          name: 'can_read',
+          head_uuid: collection_uuid,
+          properties: {}
+        }
+      },
+      headers: auth(:admin)
+    assert_response :success
+    can_read_collection_uuid = json_response['uuid']
+
+    # Should not be able read the permission link via permissions API,
+    # because permission is only can_read, not can_manage
+    get "/arvados/v1/permissions/#{collection_uuid}",
       headers: auth(:active)
     assert_response 404
 
-    get "/arvados/v1/links",
-        params: {
-          :filters => [["link_class", "=", "permission"], ["head_uuid", "=", groups(:public).uuid]].to_json
-        },
+    # Should not be able to read the permission link directly, for
+    # same reason
+    get "/arvados/v1/links/#{can_read_collection_uuid}",
       headers: auth(:active)
+    assert_response 404
+
+    ### Now add a can_manage link
+    post "/arvados/v1/links",
+      params: {
+        link: {
+          tail_uuid: users(:active).uuid,
+          link_class: 'permission',
+          name: 'can_manage',
+          head_uuid: collection_uuid,
+          properties: {}
+        }
+      },
+      headers: auth(:admin)
     assert_response :success
-    assert_equal [], json_response['items']
+    can_manage_collection_uuid = json_response['uuid']
 
-    # Should not be able to read links directly either
-    get "/arvados/v1/links/#{can_read_uuid}",
-        params: {},
+    # Should be able read both permission links via permissions API
+    get "/arvados/v1/permissions/#{collection_uuid}",
       headers: auth(:active)
-    assert_response 404
+    assert_response :success
+    perm_uuids = json_response['items'].map { |item| item['uuid'] }
+    assert_includes perm_uuids, can_read_collection_uuid, "can_read_uuid not found"
+    assert_includes perm_uuids, can_manage_collection_uuid, "can_manage_uuid not found"
+
+    # Should be able to read both permission links directly
+    [can_read_collection_uuid, can_manage_collection_uuid].each do |uuid|
+      get "/arvados/v1/links/#{uuid}",
+        headers: auth(:active)
+      assert_response :success
+    end
   end
 
   test "get_permissions returns 404 for nonexistent uuid" do
index 2ee3b3cf94cab5c73f3b4e809db8b6afac7aec81..0548a767ba4cbba9b3b94d6303b65db6cb53c7b8 100644 (file)
@@ -62,7 +62,7 @@ class SelectTest < ActionDispatch::IntegrationTest
       headers: auth(:admin)
     assert_response :success
     uuids = json_response['items'].collect { |i| i['uuid'] }
-    assert_equal uuids, uuids.sort
+    assert_equal uuids, uuids.sort.reverse
   end
 
   def assert_link_classes_ascend(current_class, prev_class)
index 1573b6862b3d50f849a54ae4f6a9ab9858754281..b08ff8ffae2abb04c146613d737ca26555b22710 100644 (file)
@@ -68,19 +68,19 @@ Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
  
      # Networking
      # WEBUI PORT
-     arv.vm.network "forwarded_port", guest: 9443, host: 9443
+     arv.vm.network "forwarded_port", guest: 443, host: 9443
      # WORKBENCH1
-     arv.vm.network "forwarded_port", guest: 9444, host: 9444
+     arv.vm.network "forwarded_port", guest: 8805, host: 9444
      # WORKBENCH2
-     arv.vm.network "forwarded_port", guest: 9445, host: 9445
+     arv.vm.network "forwarded_port", guest: 443, host: 9445
      # KEEPPROXY
-     arv.vm.network "forwarded_port", guest: 35101, host: 35101
+     arv.vm.network "forwarded_port", guest: 8801, host: 35101
      # KEEPWEB
-     arv.vm.network "forwarded_port", guest: 11002, host: 11002
+     arv.vm.network "forwarded_port", guest: 8802, host: 11002
      # WEBSHELL
-     arv.vm.network "forwarded_port", guest: 14202, host: 14202
+     arv.vm.network "forwarded_port", guest: 8803, host: 14202
      # WEBSOCKET
-     arv.vm.network "forwarded_port", guest: 18002, host: 18002
+     arv.vm.network "forwarded_port", guest: 8804, host: 18002
      arv.vm.provision "shell",
                       inline: "cp -vr /vagrant/config_examples/single_host/single_hostname /home/vagrant/local_config_dir;
                                cp -vr /vagrant/tests /home/vagrant/tests;
index 8c14c56ed3a4336a6820c9359b4477a2c348ecd0..b92c818b898eec8899ec522d3bd0f10ff34a0e89 100644 (file)
@@ -81,6 +81,13 @@ arvados:
       # required to test with arvados-snakeoil certs
       insecure: false
 
+    resources:
+      virtual_machines:
+        shell:
+          name: shell
+          backend: __SHELL_INT_IP__
+          port: 4200
+
     ### TOKENS
     tokens:
       system_root: __SYSTEM_ROOT_TOKEN__
diff --git a/tools/salt-install/config_examples/multi_host/aws/states/shell_cron_add_login_sync.sls b/tools/salt-install/config_examples/multi_host/aws/states/shell_cron_add_login_sync.sls
new file mode 100644 (file)
index 0000000..86c591e
--- /dev/null
@@ -0,0 +1,86 @@
+# Copyright (C) The Arvados Authors. All rights reserved.
+#
+# SPDX-License-Identifier: Apache-2.0
+
+# This state tries to query the controller using the parameters set in
+# the `arvados.cluster.resources.virtual_machines` pillar, to get the
+# ARVADOS_VIRTUAL_MACHINE_UUID for the host and configure the arvados login-sync cron
+# as described in https://doc.arvados.org/main/install/install-shell-server.html
+
+{%- set curr_tpldir = tpldir %}
+{%- set tpldir = 'arvados' %}
+{%- from "arvados/map.jinja" import arvados with context %}
+{%- from "arvados/libtofs.jinja" import files_switch with context %}
+{%- set tpldir = curr_tpldir %}
+
+{%- set virtual_machines = arvados.cluster.resources.virtual_machines | default({}) %}
+{%- set api_token = arvados.cluster.tokens.system_root | yaml_encode %}
+{%- set api_host = arvados.cluster.Services.Controller.ExternalURL | regex_replace('^http(s?)://', '', ignorecase=true) %}
+
+extra_shell_cron_add_login_sync_add_jq_pkg_installed:
+  pkg.installed:
+    - name: jq
+
+{%- for vm, vm_params in virtual_machines.items() %}
+  {%- set vm_name = vm_params.name | default(vm) %}
+
+  # Check if any of the specified virtual_machines parameters corresponds to this instance
+  # It should be an error if we get more than one occurrence
+  {%- if vm_name in [grains['id'], grains['host'], grains['fqdn'], grains['nodename']] or
+         vm_params.backend in [grains['id'], grains['host'], grains['fqdn'], grains['nodename']] +
+                               grains['ipv4'] + grains['ipv6'] %}
+
+    # We need to query the VM UUID
+    {%- set cmd_query_vm_uuid = 'arv --short virtual_machine list' ~
+                                ' --filters \'[["hostname", "=", "' ~ vm_name ~ '"]]\''
+    %}
+
+extra_shell_cron_add_login_sync_add_{{ vm }}_get_vm_uuid_cmd_run:
+  cmd.run:
+    - env:
+      - ARVADOS_API_TOKEN: {{ api_token }}
+      - ARVADOS_API_HOST: {{ api_host }}
+      - ARVADOS_API_HOST_INSECURE: {{ arvados.cluster.tls.insecure | default(false) }}
+    - name: {{ cmd_query_vm_uuid }} | head -1 | tee /tmp/vm_uuid_{{ vm }}
+    - unless:
+      - /bin/grep -qE "[a-z0-9]{5}-2x53u-[a-z0-9]{15}" /tmp/vm_uuid_{{ vm }}
+    - require:
+      - gem: arvados-shell-package-install-gem-arvados-cli-installed
+
+extra_shell_cron_add_login_sync_add_{{ vm }}_arvados_api_host_cron_env_present:
+  cron.env_present:
+    - name: ARVADOS_API_HOST
+    - value: {{ api_host }}
+    - onlyif:
+      - /bin/grep -qE "[a-z0-9]{5}-2x53u-[a-z0-9]{15}" /tmp/vm_uuid_{{ vm }}
+
+extra_shell_cron_add_login_sync_add_{{ vm }}_arvados_api_token_cron_env_present:
+  cron.env_present:
+    - name: ARVADOS_API_TOKEN
+    - value: {{ api_token }}
+    - onlyif:
+      - /bin/grep -qE "[a-z0-9]{5}-2x53u-[a-z0-9]{15}" /tmp/vm_uuid_{{ vm }}
+
+extra_shell_cron_add_login_sync_add_{{ vm }}_arvados_api_host_insecure_cron_env_present:
+  cron.env_present:
+    - name: ARVADOS_API_HOST_INSECURE
+    - value: {{ arvados.cluster.tls.insecure | default(false) }}
+    - onlyif:
+      - /bin/grep -qE "[a-z0-9]{5}-2x53u-[a-z0-9]{15}" /tmp/vm_uuid_{{ vm }}
+
+extra_shell_cron_add_login_sync_add_{{ vm }}_arvados_virtual_machine_uuid_cron_env_present:
+  cron.env_present:
+    - name: ARVADOS_VIRTUAL_MACHINE_UUID
+    - value: __slot__:salt:cmd.run("cat /tmp/vm_uuid_{{ vm }}")
+    - onlyif:
+      - /bin/grep -qE "[a-z0-9]{5}-2x53u-[a-z0-9]{15}" /tmp/vm_uuid_{{ vm }}
+
+extra_shell_cron_add_login_sync_add_{{ vm }}_arvados_login_sync_cron_present:
+  cron.present:
+    - name: /usr/local/bin/arvados-login-sync
+    - minute: '*/2'
+    - onlyif:
+      - /bin/grep -qE "[a-z0-9]{5}-2x53u-[a-z0-9]{15}" /tmp/vm_uuid_{{ vm }}
+
+  {%- endif %}
+{%- endfor %}
index 2579c5ffb0be5b66ee8b892fb7d25b731b82b400..d11e61bba3683b3da20560a8900764276b870db7 100644 (file)
@@ -28,7 +28,6 @@ arvados:
   ## manage OS packages with some other tool and you don't want us messing up
   ## with your setup.
   ruby:
-
     ## We set these to `true` here for testing purposes.
     ## They both default to `false`.
     manage_ruby: true
@@ -90,7 +89,7 @@ arvados:
       virtual_machines:
         shell:
           name: webshell
-          backend: 127.0.1.1
+          backend: 127.0.0.1
           port: 4200
 
     ### TOKENS
index 8e4d66caf5190ff9bc56df42ead5925f2674d438..21bb2c45a83e9d006117dd564ba1e9b916a6a080 100644 (file)
@@ -88,8 +88,8 @@ arvados:
     resources:
       virtual_machines:
         shell:
-          name: webshell
-          backend: 127.0.1.1
+          name: __HOSTNAME_EXT__
+          backend: 127.0.0.1
           port: 4200
 
     ### TOKENS
diff --git a/tools/salt-install/config_examples/single_host/single_hostname/states/shell_cron_add_login_sync.sls b/tools/salt-install/config_examples/single_host/single_hostname/states/shell_cron_add_login_sync.sls
new file mode 100644 (file)
index 0000000..86c591e
--- /dev/null
@@ -0,0 +1,86 @@
+# Copyright (C) The Arvados Authors. All rights reserved.
+#
+# SPDX-License-Identifier: Apache-2.0
+
+# This state tries to query the controller using the parameters set in
+# the `arvados.cluster.resources.virtual_machines` pillar, to get the
+# ARVADOS_VIRTUAL_MACHINE_UUID for the host and configure the arvados login-sync cron
+# as described in https://doc.arvados.org/main/install/install-shell-server.html
+
+{%- set curr_tpldir = tpldir %}
+{%- set tpldir = 'arvados' %}
+{%- from "arvados/map.jinja" import arvados with context %}
+{%- from "arvados/libtofs.jinja" import files_switch with context %}
+{%- set tpldir = curr_tpldir %}
+
+{%- set virtual_machines = arvados.cluster.resources.virtual_machines | default({}) %}
+{%- set api_token = arvados.cluster.tokens.system_root | yaml_encode %}
+{%- set api_host = arvados.cluster.Services.Controller.ExternalURL | regex_replace('^http(s?)://', '', ignorecase=true) %}
+
+extra_shell_cron_add_login_sync_add_jq_pkg_installed:
+  pkg.installed:
+    - name: jq
+
+{%- for vm, vm_params in virtual_machines.items() %}
+  {%- set vm_name = vm_params.name | default(vm) %}
+
+  # Check if any of the specified virtual_machines parameters corresponds to this instance
+  # It should be an error if we get more than one occurrence
+  {%- if vm_name in [grains['id'], grains['host'], grains['fqdn'], grains['nodename']] or
+         vm_params.backend in [grains['id'], grains['host'], grains['fqdn'], grains['nodename']] +
+                               grains['ipv4'] + grains['ipv6'] %}
+
+    # We need to query the VM UUID
+    {%- set cmd_query_vm_uuid = 'arv --short virtual_machine list' ~
+                                ' --filters \'[["hostname", "=", "' ~ vm_name ~ '"]]\''
+    %}
+
+extra_shell_cron_add_login_sync_add_{{ vm }}_get_vm_uuid_cmd_run:
+  cmd.run:
+    - env:
+      - ARVADOS_API_TOKEN: {{ api_token }}
+      - ARVADOS_API_HOST: {{ api_host }}
+      - ARVADOS_API_HOST_INSECURE: {{ arvados.cluster.tls.insecure | default(false) }}
+    - name: {{ cmd_query_vm_uuid }} | head -1 | tee /tmp/vm_uuid_{{ vm }}
+    - unless:
+      - /bin/grep -qE "[a-z0-9]{5}-2x53u-[a-z0-9]{15}" /tmp/vm_uuid_{{ vm }}
+    - require:
+      - gem: arvados-shell-package-install-gem-arvados-cli-installed
+
+extra_shell_cron_add_login_sync_add_{{ vm }}_arvados_api_host_cron_env_present:
+  cron.env_present:
+    - name: ARVADOS_API_HOST
+    - value: {{ api_host }}
+    - onlyif:
+      - /bin/grep -qE "[a-z0-9]{5}-2x53u-[a-z0-9]{15}" /tmp/vm_uuid_{{ vm }}
+
+extra_shell_cron_add_login_sync_add_{{ vm }}_arvados_api_token_cron_env_present:
+  cron.env_present:
+    - name: ARVADOS_API_TOKEN
+    - value: {{ api_token }}
+    - onlyif:
+      - /bin/grep -qE "[a-z0-9]{5}-2x53u-[a-z0-9]{15}" /tmp/vm_uuid_{{ vm }}
+
+extra_shell_cron_add_login_sync_add_{{ vm }}_arvados_api_host_insecure_cron_env_present:
+  cron.env_present:
+    - name: ARVADOS_API_HOST_INSECURE
+    - value: {{ arvados.cluster.tls.insecure | default(false) }}
+    - onlyif:
+      - /bin/grep -qE "[a-z0-9]{5}-2x53u-[a-z0-9]{15}" /tmp/vm_uuid_{{ vm }}
+
+extra_shell_cron_add_login_sync_add_{{ vm }}_arvados_virtual_machine_uuid_cron_env_present:
+  cron.env_present:
+    - name: ARVADOS_VIRTUAL_MACHINE_UUID
+    - value: __slot__:salt:cmd.run("cat /tmp/vm_uuid_{{ vm }}")
+    - onlyif:
+      - /bin/grep -qE "[a-z0-9]{5}-2x53u-[a-z0-9]{15}" /tmp/vm_uuid_{{ vm }}
+
+extra_shell_cron_add_login_sync_add_{{ vm }}_arvados_login_sync_cron_present:
+  cron.present:
+    - name: /usr/local/bin/arvados-login-sync
+    - minute: '*/2'
+    - onlyif:
+      - /bin/grep -qE "[a-z0-9]{5}-2x53u-[a-z0-9]{15}" /tmp/vm_uuid_{{ vm }}
+
+  {%- endif %}
+{%- endfor %}
index a26b3feaa3e0536266e9a4d73ed85c9196fbd792..f6e3bb3ae02f445fde34c50cad27d5eee6975e71 100755 (executable)
@@ -518,7 +518,7 @@ if [ -d "${F_DIR}"/extra/extra ]; then
     # Same when using self-signed certificates.
     SKIP_SNAKE_OIL="dont_add_snakeoil_certs"
   fi
-  for f in $(ls "${F_DIR}"/extra/extra/*.sls | egrep -v "${SKIP_SNAKE_OIL}|shell_sudo_passwordless"); do
+  for f in $(ls "${F_DIR}"/extra/extra/*.sls | egrep -v "${SKIP_SNAKE_OIL}|shell_"); do
   echo "    - extra.$(basename ${f} | sed 's/.sls$//g')" >> ${S_DIR}/top.sls
   done
   # Use byo or self-signed certificates
@@ -548,10 +548,11 @@ if [ -z "${ROLES}" ]; then
     grep -q "custom_certs"    ${S_DIR}/top.sls || echo "    - extra.custom_certs" >> ${S_DIR}/top.sls
   fi
 
-  echo "    - extra.shell_sudo_passwordless" >> ${S_DIR}/top.sls
   echo "    - postgres" >> ${S_DIR}/top.sls
   echo "    - docker.software" >> ${S_DIR}/top.sls
   echo "    - arvados" >> ${S_DIR}/top.sls
+  echo "    - extra.shell_sudo_passwordless" >> ${S_DIR}/top.sls
+  echo "    - extra.shell_cron_add_login_sync" >> ${S_DIR}/top.sls
 
   # Pillars
   echo "    - docker" >> ${P_DIR}/top.sls
@@ -759,6 +760,7 @@ else
       "shell")
         # States
         echo "    - extra.shell_sudo_passwordless" >> ${S_DIR}/top.sls
+        echo "    - extra.shell_cron_add_login_sync" >> ${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