Merge branch '18676-anon-token-improvement'
authorWard Vandewege <ward@curii.com>
Mon, 14 Feb 2022 19:29:40 +0000 (14:29 -0500)
committerWard Vandewege <ward@curii.com>
Mon, 14 Feb 2022 19:29:40 +0000 (14:29 -0500)
closes #18676

Arvados-DCO-1.1-Signed-off-by: Ward Vandewege <ward@curii.com>

doc/admin/upgrading.html.textile.liquid
doc/install/install-keep-web.html.textile.liquid
lib/boot/seed.go
lib/config/config.default.yml
lib/config/load.go
services/api/app/models/api_client_authorization.rb
services/api/app/models/database_seeds.rb
services/api/lib/current_api_client.rb
services/api/script/get_anonymous_user_token.rb [deleted file]

index 2bd41829bea9ebb20fbd359a6cce5836a93177f8..05b932a116bea3578dbf3e21563f6745eef80208 100644 (file)
@@ -35,10 +35,14 @@ TODO: extract this information based on git commit messages and generate changel
 <div class="releasenotes">
 </notextile>
 
-h2(#main). development main (as of 2021-11-10)
+h2(#main). development main (as of 2022-02-10)
 
 "previous: Upgrading from 2.3.0":#v2_3_0
 
+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.
+
 h3. Preemptible instance types are used automatically, if any are configured
 
 The default behavior for selecting "preemptible instances":{{site.baseurl}}/admin/spot-instances.html has changed. If your configuration lists any instance types with @Preemptible: true@, all child (non-top-level) containers will automatically be scheduled on preemptible instances. To avoid using preemptible instances except when explicitly requested by clients, add @AlwaysUsePreemptibleInstances: false@ in the @Containers@ config section. (Previously, preemptible instance types were never used unless the configuration specified @UsePreemptibleInstances: true@. That flag has been removed.)
index ea2ffb5e4889a4329fa6cc94f0d9a474722aa7ba..1ba9fc522fab9dbf69207d3fa67061e586843079 100644 (file)
@@ -11,7 +11,7 @@ SPDX-License-Identifier: CC-BY-SA-3.0
 
 # "Introduction":#introduction
 # "Configure DNS":#introduction
-# "Configure anonymous user token.yml":#update-config
+# "Configure anonymous user token":#update-config
 # "Update nginx configuration":#update-nginx
 # "Install keep-web package":#install-packages
 # "Start the service":#start-service
@@ -105,16 +105,13 @@ h2. Set InternalURLs
 
 h2(#update-config). Configure anonymous user token
 
-{% assign railscmd = "bin/bundle exec ./script/get_anonymous_user_token.rb --get" %}
-{% assign railsout = "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz" %}
 If you intend to use Keep-web to serve public data to anonymous clients, configure it with an anonymous token.
 
-# First, generate a long random string and put it in the @config.yml@ file, in the @AnonymousUserToken@ field.
-# Then, use the following command on the <strong>API server</strong> to register the anonymous user token in the database. {% include 'install_rails_command' %}
+Generate a random string (>= 32 characters long) and put it in the @config.yml@ file, in the @AnonymousUserToken@ field.
 
 <notextile>
 <pre><code>    Users:
-      AnonymousUserToken: <span class="userinput">"{{railsout}}"</span>
+      AnonymousUserToken: <span class="userinput">"zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz"</span>
 </code></pre>
 </notextile>
 
index bd1e942658e9f50fba873d3de4f3a1c971dd54dc..b43d907201d47a44c013ec7d201955f7f5145377 100644 (file)
@@ -27,9 +27,5 @@ func (seedDatabase) Run(ctx context.Context, fail func(error), super *Supervisor
        if err != nil {
                return err
        }
-       err = super.RunProgram(ctx, "services/api", runOptions{env: railsEnv}, "bundle", "exec", "./script/get_anonymous_user_token.rb")
-       if err != nil {
-               return err
-       }
        return nil
 }
index 17bba5410bb4f31efceb8b1b6ed74eb372d183b8..a7ce9828573fb4b5832f49837de19413af4aa6d4 100644 (file)
@@ -294,9 +294,7 @@ Clusters:
       NewInactiveUserNotificationRecipients: {}
 
       # Set AnonymousUserToken to enable anonymous user access. Populate this
-      # field with a long random string. Then run "bundle exec
-      # ./script/get_anonymous_user_token.rb" in the directory where your API
-      # server is running to record the token in the database.
+      # field with a random string at least 50 characters long.
       AnonymousUserToken: ""
 
       # If a new user has an alternate email address (local@domain)
index c2eb55554488f465a7dc990b87ad6da450459a4d..aa7520ca29e7d6fe2d4040e50ae11f3416b98c77 100644 (file)
@@ -299,9 +299,10 @@ func (ldr *Loader) Load() (*arvados.Config, error) {
                for _, err = range []error{
                        ldr.checkClusterID(fmt.Sprintf("Clusters.%s", id), id, false),
                        ldr.checkClusterID(fmt.Sprintf("Clusters.%s.Login.LoginCluster", id), cc.Login.LoginCluster, true),
-                       ldr.checkToken(fmt.Sprintf("Clusters.%s.ManagementToken", id), cc.ManagementToken),
-                       ldr.checkToken(fmt.Sprintf("Clusters.%s.SystemRootToken", id), cc.SystemRootToken),
-                       ldr.checkToken(fmt.Sprintf("Clusters.%s.Collections.BlobSigningKey", id), cc.Collections.BlobSigningKey),
+                       ldr.checkToken(fmt.Sprintf("Clusters.%s.ManagementToken", id), cc.ManagementToken, true),
+                       ldr.checkToken(fmt.Sprintf("Clusters.%s.SystemRootToken", id), cc.SystemRootToken, true),
+                       ldr.checkToken(fmt.Sprintf("Clusters.%s.Users.AnonymousUserToken", id), cc.Users.AnonymousUserToken, false),
+                       ldr.checkToken(fmt.Sprintf("Clusters.%s.Collections.BlobSigningKey", id), cc.Collections.BlobSigningKey, true),
                        checkKeyConflict(fmt.Sprintf("Clusters.%s.PostgreSQL.Connection", id), cc.PostgreSQL.Connection),
                        ldr.checkEnum("Containers.LocalKeepLogsToContainerLog", cc.Containers.LocalKeepLogsToContainerLog, "none", "all", "errors"),
                        ldr.checkEmptyKeepstores(cc),
@@ -333,10 +334,15 @@ func (ldr *Loader) checkClusterID(label, clusterID string, emptyStringOk bool) e
 var acceptableTokenRe = regexp.MustCompile(`^[a-zA-Z0-9]+$`)
 var acceptableTokenLength = 32
 
-func (ldr *Loader) checkToken(label, token string) error {
-       if token == "" {
-               if ldr.Logger != nil {
-                       ldr.Logger.Warnf("%s: secret token is not set (use %d+ random characters from a-z, A-Z, 0-9)", label, acceptableTokenLength)
+func (ldr *Loader) checkToken(label, token string, mandatory bool) error {
+       if len(token) == 0 {
+               if !mandatory {
+                       // when a token is not mandatory, the acceptable length and content is only checked if its length is non-zero
+                       return nil
+               } else {
+                       if ldr.Logger != nil {
+                               ldr.Logger.Warnf("%s: secret token is not set (use %d+ random characters from a-z, A-Z, 0-9)", label, acceptableTokenLength)
+                       }
                }
        } else if !acceptableTokenRe.MatchString(token) {
                return fmt.Errorf("%s: unacceptable characters in token (only a-z, A-Z, 0-9 are acceptable)", label)
index 7c7ed759c60058b5915ad1d56505dba6b56d84dd..f8454029d6b8cf2561505080ac5b74b8d57b8c70 100644 (file)
@@ -111,6 +111,31 @@ class ApiClientAuthorization < ArvadosModel
     clnt
   end
 
+  def self.check_anonymous_user_token token
+    case token[0..2]
+    when 'v2/'
+      _, token_uuid, secret, optional = token.split('/')
+      unless token_uuid.andand.length == 27 && secret.andand.length.andand > 0 &&
+             token_uuid == Rails.configuration.ClusterID+"-gj3su-anonymouspublic"
+        # invalid v2 token, or v2 token for another user
+        return nil
+      end
+    else
+      # v1 token
+      secret = token
+    end
+
+    # The anonymous token content and minimum length is verified in lib/config
+    if secret.length >= 0 && secret == Rails.configuration.Users.AnonymousUserToken
+      return ApiClientAuthorization.new(user: User.find_by_uuid(anonymous_user_uuid),
+                                        uuid: Rails.configuration.ClusterID+"-gj3su-anonymouspublic",
+                                        api_token: token,
+                                        api_client: anonymous_user_token_api_client)
+    else
+      return nil
+    end
+  end
+
   def self.check_system_root_token token
     if token == Rails.configuration.SystemRootToken
       return ApiClientAuthorization.new(user: User.find_by_uuid(system_user_uuid),
@@ -126,6 +151,11 @@ class ApiClientAuthorization < ArvadosModel
     return nil if token.nil? or token.empty?
     remote ||= Rails.configuration.ClusterID
 
+    auth = self.check_anonymous_user_token(token)
+    if !auth.nil?
+      return auth
+    end
+
     auth = self.check_system_root_token(token)
     if !auth.nil?
       return auth
index 67bd3d10d78975cd942a32acc7bb49306d31e0cc..e0ae850ae7b10412eaa5a7578601de5da0f5e064 100644 (file)
@@ -14,6 +14,7 @@ class DatabaseSeeds
       anonymous_group
       anonymous_group_read_permission
       anonymous_user
+      anonymous_user_token_api_client
       system_root_token_api_client
       public_project_group
       public_project_read_permission
index 37e86976c1d9c5032d1948b415290069def7e1b3..ee666b77ab78632f843211fc9e510f9dd11f564c 100644 (file)
@@ -225,6 +225,16 @@ module CurrentApiClient
     end
   end
 
+  def anonymous_user_token_api_client
+    $anonymous_user_token_api_client = check_cache $anonymous_user_token_api_client do
+      act_as_system_user do
+        ActiveRecord::Base.transaction do
+          ApiClient.find_or_create_by!(is_trusted: false, url_prefix: "", name: "AnonymousUserToken")
+        end
+      end
+    end
+  end
+
   def system_root_token_api_client
     $system_root_token_api_client = check_cache $system_root_token_api_client do
       act_as_system_user do
diff --git a/services/api/script/get_anonymous_user_token.rb b/services/api/script/get_anonymous_user_token.rb
deleted file mode 100755 (executable)
index 4c3ca34..0000000
+++ /dev/null
@@ -1,85 +0,0 @@
-#!/usr/bin/env ruby
-# Copyright (C) The Arvados Authors. All rights reserved.
-#
-# SPDX-License-Identifier: AGPL-3.0
-
-# Get or Create an anonymous user token.
-# If get option is used, an existing anonymous user token is returned. If none exist, one is created.
-# If the get option is omitted, a new token is created and returned.
-
-require 'optimist'
-
-opts = Optimist::options do
-  banner ''
-  banner "Usage: get_anonymous_user_token "
-  banner ''
-  opt :get, <<-eos
-Get an existing anonymous user token. If no such token exists \
-or if this option is omitted, a new token is created and returned.
-  eos
-  opt :token, "token to create (optional)", :type => :string
-end
-
-get_existing = opts[:get]
-supplied_token = opts[:token]
-
-require File.dirname(__FILE__) + '/../config/environment'
-
-include ApplicationHelper
-act_as_system_user
-
-def create_api_client_auth(supplied_token=nil)
-  supplied_token = Rails.configuration.Users["AnonymousUserToken"]
-
-  if supplied_token.nil? or supplied_token.empty?
-    puts "Users.AnonymousUserToken is empty.  Destroying tokens that belong to anonymous."
-    # Token is empty.  Destroy any anonymous tokens.
-    ApiClientAuthorization.where(user: anonymous_user).destroy_all
-    return nil
-  end
-
-  attr = {user: anonymous_user,
-          api_client_id: 0,
-          scopes: ['GET /']}
-
-  secret = supplied_token
-
-  if supplied_token[0..2] == 'v2/'
-    _, token_uuid, secret, optional = supplied_token.split('/')
-    if token_uuid[0..4] != Rails.configuration.ClusterID
-      # Belongs to a different cluster.
-      puts supplied_token
-      return nil
-    end
-    attr[:uuid] = token_uuid
-  end
-
-  attr[:api_token] = secret
-
-  api_client_auth = ApiClientAuthorization.where(attr).first
-  if !api_client_auth
-    # The anonymous user token should never expire but we are not allowed to
-    # set :expires_at to nil, so we set it to 1000 years in the future.
-    attr[:expires_at] = Time.now + 1000.years
-    api_client_auth = ApiClientAuthorization.create!(attr)
-  end
-  api_client_auth
-end
-
-if get_existing
-  api_client_auth = ApiClientAuthorization.
-    where('user_id=?', anonymous_user.id.to_i).
-    where('expires_at>?', Time.now).
-    select { |auth| auth.scopes == ['GET /'] }.
-    first
-end
-
-# either not a get or no api_client_auth was found
-if !api_client_auth
-  api_client_auth = create_api_client_auth(supplied_token)
-end
-
-# print it to the console
-if api_client_auth
-  puts "v2/#{api_client_auth.uuid}/#{api_client_auth.api_token}"
-end