Merge branch '16726-anon-user-token' refs #16726
authorPeter Amstutz <peter.amstutz@curii.com>
Thu, 3 Sep 2020 13:41:02 +0000 (09:41 -0400)
committerPeter Amstutz <peter.amstutz@curii.com>
Thu, 3 Sep 2020 13:41:02 +0000 (09:41 -0400)
Arvados-DCO-1.1-Signed-off-by: Peter Amstutz <peter.amstutz@curii.com>

lib/boot/seed.go
lib/boot/supervisor.go
lib/controller/integration_test.go
services/api/app/models/api_client_authorization.rb
services/api/script/get_anonymous_user_token.rb
services/api/test/integration/remote_user_test.rb

index d1cf2a870975b662d8318d9ef6a25f08ce204c93..2afccc45b628cc01b00ddac873abdfc4eae20b61 100644 (file)
@@ -24,5 +24,9 @@ func (seedDatabase) Run(ctx context.Context, fail func(error), super *Supervisor
        if err != nil {
                return err
        }
+       err = super.RunProgram(ctx, "services/api", nil, railsEnv, "bundle", "exec", "./script/get_anonymous_user_token.rb")
+       if err != nil {
+               return err
+       }
        return nil
 }
index 3f4fb7482229bc704e9daee1d71a0775aa8ed3fa..3484a1444e786cc5f026f0d0a68ada822b79ffb1 100644 (file)
@@ -617,6 +617,10 @@ func (super *Supervisor) autofillConfig(cfg *arvados.Config) error {
        if cluster.Collections.BlobSigningKey == "" {
                cluster.Collections.BlobSigningKey = randomHexString(64)
        }
+       if cluster.Users.AnonymousUserToken == "" {
+               cluster.Users.AnonymousUserToken = randomHexString(64)
+       }
+
        if super.ClusterType != "production" && cluster.Containers.DispatchPrivateKey == "" {
                buf, err := ioutil.ReadFile(filepath.Join(super.SourcePath, "lib", "dispatchcloud", "test", "sshkey_dispatch"))
                if err != nil {
index a73f5f9f828574b1c234932432a2a4b63c769087..03c885092550167c522432e36003aafcd2a05730 100644 (file)
@@ -8,6 +8,7 @@ import (
        "bytes"
        "context"
        "encoding/json"
+       "fmt"
        "io"
        "math"
        "net"
@@ -139,10 +140,15 @@ func (s *IntegrationSuite) TearDownSuite(c *check.C) {
        }
 }
 
+// Get rpc connection struct initialized to communicate with the
+// specified cluster.
 func (s *IntegrationSuite) conn(clusterID string) *rpc.Conn {
        return rpc.NewConn(clusterID, s.testClusters[clusterID].controllerURL, true, rpc.PassthroughTokenProvider)
 }
 
+// Return Context, Arvados.Client and keepclient structs initialized
+// to connect to the specified cluster (by clusterID) using with the supplied
+// Arvados token.
 func (s *IntegrationSuite) clientsWithToken(clusterID string, token string) (context.Context, *arvados.Client, *keepclient.KeepClient) {
        cl := s.testClusters[clusterID].config.Clusters[clusterID]
        ctx := auth.NewContext(context.Background(), auth.NewCredentials(token))
@@ -159,6 +165,10 @@ func (s *IntegrationSuite) clientsWithToken(clusterID string, token string) (con
        return ctx, ac, kc
 }
 
+// Log in as a user called "example", get the user's API token,
+// initialize clients with the API token, set up the user and
+// optionally activate the user.  Return client structs for
+// communicating with the cluster on behalf of the 'example' user.
 func (s *IntegrationSuite) userClients(rootctx context.Context, c *check.C, conn *rpc.Conn, clusterID string, activate bool) (context.Context, *arvados.Client, *keepclient.KeepClient) {
        login, err := conn.UserSessionCreate(rootctx, rpc.UserSessionCreateOptions{
                ReturnTo: ",https://example.com",
@@ -192,10 +202,18 @@ func (s *IntegrationSuite) userClients(rootctx context.Context, c *check.C, conn
        return ctx, ac, kc
 }
 
+// Return Context, arvados.Client and keepclient structs initialized
+// to communicate with the cluster as the system root user.
 func (s *IntegrationSuite) rootClients(clusterID string) (context.Context, *arvados.Client, *keepclient.KeepClient) {
        return s.clientsWithToken(clusterID, s.testClusters[clusterID].config.Clusters[clusterID].SystemRootToken)
 }
 
+// Return Context, arvados.Client and keepclient structs initialized
+// to communicate with the cluster as the anonymous user.
+func (s *IntegrationSuite) anonymousClients(clusterID string) (context.Context, *arvados.Client, *keepclient.KeepClient) {
+       return s.clientsWithToken(clusterID, s.testClusters[clusterID].config.Clusters[clusterID].Users.AnonymousUserToken)
+}
+
 func (s *IntegrationSuite) TestGetCollectionByPDH(c *check.C) {
        conn1 := s.conn("z1111")
        rootctx1, _, _ := s.rootClients("z1111")
@@ -234,6 +252,71 @@ func (s *IntegrationSuite) TestGetCollectionByPDH(c *check.C) {
        c.Check(coll.PortableDataHash, check.Equals, pdh)
 }
 
+func (s *IntegrationSuite) TestGetCollectionAsAnonymous(c *check.C) {
+       conn1 := s.conn("z1111")
+       conn3 := s.conn("z3333")
+       rootctx1, rootac1, rootkc1 := s.rootClients("z1111")
+       anonctx3, anonac3, _ := s.anonymousClients("z3333")
+
+       // Make sure anonymous token was set
+       c.Assert(anonac3.AuthToken, check.Not(check.Equals), "")
+
+       // Create the collection to find its PDH (but don't save it
+       // anywhere yet)
+       var coll1 arvados.Collection
+       fs1, err := coll1.FileSystem(rootac1, rootkc1)
+       c.Assert(err, check.IsNil)
+       f, err := fs1.OpenFile("test.txt", os.O_CREATE|os.O_RDWR, 0777)
+       c.Assert(err, check.IsNil)
+       _, err = io.WriteString(f, "IntegrationSuite.TestGetCollectionAsAnonymous")
+       c.Assert(err, check.IsNil)
+       err = f.Close()
+       c.Assert(err, check.IsNil)
+       mtxt, err := fs1.MarshalManifest(".")
+       c.Assert(err, check.IsNil)
+       pdh := arvados.PortableDataHash(mtxt)
+
+       // Save the collection on cluster z1111.
+       coll1, err = conn1.CollectionCreate(rootctx1, arvados.CreateOptions{Attrs: map[string]interface{}{
+               "manifest_text": mtxt,
+       }})
+       c.Assert(err, check.IsNil)
+
+       // Share it with the anonymous users group.
+       var outLink arvados.Link
+       err = rootac1.RequestAndDecode(&outLink, "POST", "/arvados/v1/links", nil,
+               map[string]interface{}{"link": map[string]interface{}{
+                       "link_class": "permission",
+                       "name":       "can_read",
+                       "tail_uuid":  "z1111-j7d0g-anonymouspublic",
+                       "head_uuid":  coll1.UUID,
+               },
+               })
+       c.Check(err, check.IsNil)
+
+       // Current user should be z3 anonymous user
+       outUser, err := anonac3.CurrentUser()
+       c.Check(err, check.IsNil)
+       c.Check(outUser.UUID, check.Equals, "z3333-tpzed-anonymouspublic")
+
+       // Get the token uuid
+       var outAuth arvados.APIClientAuthorization
+       err = anonac3.RequestAndDecode(&outAuth, "GET", "/arvados/v1/api_client_authorizations/current", nil, nil)
+       c.Check(err, check.IsNil)
+
+       // Make a v2 token of the z3 anonymous user, and use it on z1
+       _, anonac1, _ := s.clientsWithToken("z1111", fmt.Sprintf("v2/%v/%v", outAuth.UUID, outAuth.APIToken))
+       outUser2, err := anonac1.CurrentUser()
+       c.Check(err, check.IsNil)
+       // z3 anonymous user will be mapped to the z1 anonymous user
+       c.Check(outUser2.UUID, check.Equals, "z1111-tpzed-anonymouspublic")
+
+       // Retrieve the collection (which is on z1) using anonymous from cluster z3333.
+       coll, err := conn3.CollectionGet(anonctx3, arvados.GetOptions{UUID: coll1.UUID})
+       c.Check(err, check.IsNil)
+       c.Check(coll.PortableDataHash, check.Equals, pdh)
+}
+
 // 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 c31f097828b8b0401aab0c799720e76483a741f3..ab6fd8000c1f4a966be7e01450402719f7182d5a 100644 (file)
@@ -226,6 +226,11 @@ class ApiClientAuthorization < ArvadosModel
       # Add or update user and token in local database so we can
       # validate subsequent requests faster.
 
+      if remote_user['uuid'][-22..-1] == '-tpzed-anonymouspublic'
+        # Special case: map the remote anonymous user to local anonymous user
+        remote_user['uuid'] = anonymous_user_uuid
+      end
+
       user = User.find_by_uuid(remote_user['uuid'])
 
       if !user
@@ -257,6 +262,11 @@ class ApiClientAuthorization < ArvadosModel
         user.send(attr+'=', remote_user[attr])
       end
 
+      if remote_user['uuid'][-22..-1] == '-tpzed-000000000000000'
+        user.first_name = "root"
+        user.last_name = "from cluster #{remote_user_prefix}"
+      end
+
       act_as_system_user do
         if user.is_active && !remote_user['is_active']
           user.unsetup
index 4bb91e244635d7c6e10dcdff32b72264141ff9de..8775ae59594402a6231ce3169e78669fc49f2740 100755 (executable)
@@ -29,27 +29,37 @@ include ApplicationHelper
 act_as_system_user
 
 def create_api_client_auth(supplied_token=nil)
+  supplied_token = Rails.configuration.Users["AnonymousUserToken"]
 
-  # If token is supplied, see if it exists
-  if supplied_token
-    api_client_auth = ApiClientAuthorization.
-      where(api_token: supplied_token).
-      first
-    if !api_client_auth
-      # fall through to create a token
-    else
-      raise "Token exists, aborting!"
+  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
 
-  api_client_auth = ApiClientAuthorization.
-    new(user: anonymous_user,
-        api_client_id: 0,
-        expires_at: Time.now + 100.years,
-        scopes: ['GET /'],
-        api_token: supplied_token)
-  api_client_auth.save!
-  api_client_auth.reload
+  attr[:api_token] = secret
+
+  api_client_auth = ApiClientAuthorization.where(attr).first
+  if !api_client_auth
+    api_client_auth = ApiClientAuthorization.create!(attr)
+  end
   api_client_auth
 end
 
@@ -67,4 +77,6 @@ if !api_client_auth
 end
 
 # print it to the console
-puts api_client_auth.api_token
+if api_client_auth
+  puts "v2/#{api_client_auth.uuid}/#{api_client_auth.api_token}"
+end
index 04a45420fd4b768c105e89f8bd600739d69c8a6f..8ad09894a16df4bc6281ba95db6c8b6b25be589c 100644 (file)
@@ -79,7 +79,7 @@ class RemoteUsersTest < ActionDispatch::IntegrationTest
     Arvados::V1::SchemaController.any_instance.stubs(:root_url).returns "https://#{@remote_host[0]}"
     @stub_status = 200
     @stub_content = {
-      uuid: 'zbbbb-tpzed-000000000000000',
+      uuid: 'zbbbb-tpzed-000000000000001',
       email: 'foo@example.com',
       username: 'barney',
       is_admin: true,
@@ -98,7 +98,7 @@ class RemoteUsersTest < ActionDispatch::IntegrationTest
       params: {format: 'json'},
       headers: auth(remote: 'zbbbb')
     assert_response :success
-    assert_equal 'zbbbb-tpzed-000000000000000', json_response['uuid']
+    assert_equal 'zbbbb-tpzed-000000000000001', json_response['uuid']
     assert_equal false, json_response['is_admin']
     assert_equal false, json_response['is_active']
     assert_equal 'foo@example.com', json_response['email']
@@ -286,12 +286,12 @@ class RemoteUsersTest < ActionDispatch::IntegrationTest
       params: {format: 'json'},
       headers: auth(remote: 'zbbbb')
     assert_response :success
-    assert_equal 'zbbbb-tpzed-000000000000000', json_response['uuid']
+    assert_equal 'zbbbb-tpzed-000000000000001', json_response['uuid']
     assert_equal false, json_response['is_admin']
     assert_equal false, json_response['is_active']
     assert_equal 'foo@example.com', json_response['email']
     assert_equal 'barney', json_response['username']
-    post '/arvados/v1/users/zbbbb-tpzed-000000000000000/activate',
+    post '/arvados/v1/users/zbbbb-tpzed-000000000000001/activate',
       params: {format: 'json'},
       headers: auth(remote: 'zbbbb')
     assert_response 422
@@ -303,7 +303,7 @@ class RemoteUsersTest < ActionDispatch::IntegrationTest
       params: {format: 'json'},
       headers: auth(remote: 'zbbbb')
     assert_response :success
-    assert_equal 'zbbbb-tpzed-000000000000000', json_response['uuid']
+    assert_equal 'zbbbb-tpzed-000000000000001', json_response['uuid']
     assert_equal false, json_response['is_admin']
     assert_equal true, json_response['is_active']
     assert_equal 'foo@example.com', json_response['email']
@@ -316,7 +316,7 @@ class RemoteUsersTest < ActionDispatch::IntegrationTest
       params: {format: 'json'},
       headers: auth(remote: 'zbbbb')
     assert_response :success
-    assert_equal 'zbbbb-tpzed-000000000000000', json_response['uuid']
+    assert_equal 'zbbbb-tpzed-000000000000001', json_response['uuid']
     assert_equal true, json_response['is_admin']
     assert_equal true, json_response['is_active']
     assert_equal 'foo@example.com', json_response['email']
@@ -412,4 +412,22 @@ class RemoteUsersTest < ActionDispatch::IntegrationTest
     end
   end
 
+  test 'authenticate with remote token, remote user is system user' do
+    @stub_content[:uuid] = 'zbbbb-tpzed-000000000000000'
+    get '/arvados/v1/users/current',
+      params: {format: 'json'},
+      headers: auth(remote: 'zbbbb')
+    assert_equal 'from cluster zbbbb', json_response['last_name']
+  end
+
+  test 'authenticate with remote token, remote user is anonymous user' do
+    @stub_content[:uuid] = 'zbbbb-tpzed-anonymouspublic'
+    get '/arvados/v1/users/current',
+      params: {format: 'json'},
+      headers: auth(remote: 'zbbbb')
+    assert_response :success
+    assert_equal 'zzzzz-tpzed-anonymouspublic', json_response['uuid']
+  end
+
+
 end