Merge branch 'master' into 3408-production-datamanager
[arvados.git] / services / api / test / unit / log_test.rb
index 1554e0afec6515884276ec5ae355209851feba9c..d6b76fc6057633e28f8518aa9765a28ab121e3e2 100644 (file)
@@ -15,26 +15,30 @@ class LogTest < ActiveSupport::TestCase
   end
 
   def assert_properties(test_method, event, props, *keys)
-    verb = (test_method == :assert_nil) ? 'not include' : 'include'
+    verb = (test_method == :assert_nil) ? 'have nil' : 'define'
     keys.each do |prop_name|
+      assert_includes(props, prop_name, "log properties missing #{prop_name}")
       self.send(test_method, props[prop_name],
                 "#{event.to_s} log should #{verb} #{prop_name}")
     end
   end
 
+  def get_logs_about(thing)
+    Log.where(object_uuid: thing.uuid).order("created_at ASC").all
+  end
+
   def assert_logged(thing, event_type)
-    logs = Log.where(object_uuid: thing.uuid).order("created_at ASC").all
+    logs = get_logs_about(thing)
     assert_equal(@log_count, logs.size, "log count mismatch")
     @log_count += 1
     log = logs.last
     props = log.properties
-    assert_equal(system_user_uuid, log.owner_uuid,
-                 "log is not owned by system user")
+    assert_equal(current_user.andand.uuid, log.owner_uuid,
+                 "log is not owned by current user")
     assert_equal(current_user.andand.uuid, log.modified_by_user_uuid,
                  "log is not 'modified by' current user")
     assert_equal(current_api_client.andand.uuid, log.modified_by_client_uuid,
                  "log is not 'modified by' current client")
-    assert_equal(thing.kind, log.object_kind, "log kind mismatch")
     assert_equal(thing.uuid, log.object_uuid, "log UUID mismatch")
     assert_equal(event_type.to_s, log.event_type, "log event type mismatch")
     time_method, old_props_test, new_props_test = EVENT_TEST_METHODS[event_type]
@@ -50,11 +54,15 @@ class LogTest < ActiveSupport::TestCase
     yield props if block_given?
   end
 
-  def set_user_from_auth(auth_name)
-    client_auth = api_client_authorizations(auth_name)
-    Thread.current[:api_client_authorization] = client_auth
-    Thread.current[:api_client] = client_auth.api_client
-    Thread.current[:user] = client_auth.user
+  def assert_auth_logged_with_clean_properties(auth, event_type)
+    assert_logged(auth, event_type) do |props|
+      ['old_attributes', 'new_attributes'].map { |k| props[k] }.compact
+        .each do |attributes|
+        refute_includes(attributes, 'api_token',
+                        "auth log properties include sensitive API token")
+      end
+      yield props if block_given?
+    end
   end
 
   test "creating a user makes a log" do
@@ -86,11 +94,26 @@ class LogTest < ActiveSupport::TestCase
     end
   end
 
+  test "old_attributes preserves values deep inside a hash" do
+    set_user_from_auth :active
+    it = specimens(:owned_by_active_user)
+    it.properties = {'foo' => {'bar' => ['baz', 'qux', {'quux' => 'bleat'}]}}
+    it.save!
+    @log_count += 1
+    it.properties['foo']['bar'][2]['quux'] = 'blert'
+    it.save!
+    assert_logged it, :update do |props|
+      assert_equal 'bleat', props['old_attributes']['properties']['foo']['bar'][2]['quux']
+      assert_equal 'blert', props['new_attributes']['properties']['foo']['bar'][2]['quux']
+    end
+  end
+
   test "destroying an authorization makes a log" do
     set_user_from_auth :admin_trustedclient
     auth = api_client_authorizations(:spectator)
     orig_etag = auth.etag
     orig_attrs = auth.attributes
+    orig_attrs.delete 'api_token'
     auth.destroy
     assert_logged(auth, :destroy) do |props|
       assert_equal(orig_etag, props['old_etag'], "destroyed auth etag mismatch")
@@ -136,4 +159,99 @@ class LogTest < ActiveSupport::TestCase
                    "group final name mismatch")
     end
   end
+
+  test "making a log doesn't get logged" do
+    set_user_from_auth :active_trustedclient
+    log = Log.new
+    log.save!
+    assert_equal(0, get_logs_about(log).size, "made a Log about a Log")
+  end
+
+  test "non-admins can't modify or delete logs" do
+    set_user_from_auth :active_trustedclient
+    log = Log.new(summary: "immutable log test")
+    assert_nothing_raised { log.save! }
+    log.summary = "log mutation test should fail"
+    assert_raise(ArvadosModel::PermissionDeniedError) { log.save! }
+    assert_raise(ArvadosModel::PermissionDeniedError) { log.destroy }
+  end
+
+  test "admins can modify and delete logs" do
+    set_user_from_auth :admin_trustedclient
+    log = Log.new(summary: "admin log mutation test")
+    assert_nothing_raised { log.save! }
+    log.summary = "admin mutated log test"
+    assert_nothing_raised { log.save! }
+    assert_nothing_raised { log.destroy }
+  end
+
+  test "failure saving log causes failure saving object" do
+    Log.class_eval do
+      alias_method :_orig_validations, :perform_validations
+      def perform_validations(options)
+        false
+      end
+    end
+    begin
+      set_user_from_auth :active_trustedclient
+      user = users(:active)
+      user.first_name = 'Test'
+      assert_raise(ActiveRecord::RecordInvalid) { user.save! }
+    ensure
+      Log.class_eval do
+        alias_method :perform_validations, :_orig_validations
+      end
+    end
+  end
+
+  test "don't log changes only to ApiClientAuthorization.last_used_*" do
+    set_user_from_auth :admin_trustedclient
+    auth = api_client_authorizations(:spectator)
+    start_log_count = get_logs_about(auth).size
+    auth.last_used_at = Time.now
+    auth.last_used_by_ip_address = '::1'
+    auth.save!
+    assert_equal(start_log_count, get_logs_about(auth).size,
+                 "log count changed after 'using' ApiClientAuthorization")
+    auth.created_by_ip_address = '::1'
+    auth.save!
+    assert_logged(auth, :update)
+  end
+
+  test "token isn't included in ApiClientAuthorization logs" do
+    set_user_from_auth :admin_trustedclient
+    auth = ApiClientAuthorization.new
+    auth.user = users(:spectator)
+    auth.api_client = api_clients(:untrusted)
+    auth.save!
+    assert_auth_logged_with_clean_properties(auth, :create)
+    auth.expires_at = Time.now
+    auth.save!
+    assert_auth_logged_with_clean_properties(auth, :update)
+    auth.destroy
+    assert_auth_logged_with_clean_properties(auth, :destroy)
+  end
+
+  test "use ownership and permission links to determine which logs a user can see" do
+    c = Log.readable_by(users(:admin)).order("id asc").each.to_a
+    assert_equal 6, c.size
+    assert_equal 1, c[0].id # no-op
+    assert_equal 2, c[1].id # admin changes repository foo, which is owned by active user
+    assert_equal 3, c[2].id # admin changes specimen owned_by_spectator
+    assert_equal 4, c[3].id # foo collection added, readable by active through link
+    assert_equal 5, c[4].id # baz collection added, readable by active and spectator through group 'all users' group membership
+    assert_equal 6, c[5].id # log_owned_by_active
+
+    c = Log.readable_by(users(:active)).order("id asc").each.to_a
+    assert_equal 4, c.size
+    assert_equal 2, c[0].id # admin changes repository foo, which is owned by active user
+    assert_equal 4, c[1].id # foo collection added, readable by active through link
+    assert_equal 5, c[2].id # baz collection added, readable by active and spectator through group 'all users' group membership
+    assert_equal 6, c[3].id # log_owned_by_active
+
+    c = Log.readable_by(users(:spectator)).order("id asc").each.to_a
+    assert_equal 2, c.size
+    assert_equal 3, c[0].id # admin changes specimen owned_by_spectator
+    assert_equal 5, c[1].id # baz collection added, readable by active and spectator through group 'all users' group membership
+  end
 end