Browse Source

Fixed cache invalidation bug race condition: Changes to group relation participant are not reflected to the other side and FE updates will be incomplete/outdated.

Thorsten Eckel 6 years ago
parent
commit
5865d94339

+ 64 - 0
app/models/concerns/has_group_relation_definition.rb

@@ -0,0 +1,64 @@
+# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/
+module HasGroupRelationDefinition
+  extend ActiveSupport::Concern
+
+  included do
+
+    self.table_name   = "groups_#{group_relation_model_identifier}s"
+    self.primary_keys = ref_key, :group_id, :access
+
+    belongs_to group_relation_model_identifier
+    belongs_to :group
+
+    validates :access, presence: true
+    validate :validate_access
+
+    after_save :touch_related
+    after_destroy :touch_related
+  end
+
+  private
+
+  def group_relation_instance
+    @group_relation_instance ||= send(group_relation_model_identifier)
+  end
+
+  def group_relation_model_identifier
+    @group_relation_model_identifier ||= self.class.group_relation_model_identifier
+  end
+
+  def touch_related
+    # rubocop:disable Rails/SkipsModelValidations
+    group.touch if group&.persisted?
+    group_relation_instance.touch if group_relation_instance&.persisted?
+    # rubocop:enable Rails/SkipsModelValidations
+  end
+
+  def validate_access
+    query = self.class.where(
+      group_relation_model_identifier => group_relation_instance,
+      group: group
+    )
+
+    query = if access == 'full'
+              query.where.not(access: 'full')
+            else
+              query.where(access: 'full')
+            end
+
+    return if !query.exists?
+    errors.add(:access, "#{group_relation_model_identifier.to_s.capitalize} can have full or granular access to group")
+  end
+
+  # methods defined here are going to extend the class, not the instance of it
+  class_methods do
+
+    def group_relation_model_identifier
+      @group_relation_model_identifier ||= model_name.singular.split('_').first.to_sym
+    end
+
+    def ref_key
+      @ref_key ||= "#{group_relation_model_identifier}_id".to_sym
+    end
+  end
+end

+ 6 - 2
app/models/concerns/has_groups.rb

@@ -7,8 +7,12 @@ module HasGroups
 
     attr_accessor :group_access_buffer
 
-    after_create :process_group_access_buffer
-    after_update :process_group_access_buffer
+    after_save :process_group_access_buffer
+
+    # add association to Group, too but ignore it in asset output
+    Group.has_many group_through_identifier
+    Group.has_many model_name.collection.to_sym, through: group_through_identifier, after_add: :cache_update, after_remove: :cache_update, dependent: :destroy
+    Group.association_attributes_ignored group_through_identifier
 
     association_attributes_ignored :groups, group_through_identifier
 

+ 4 - 4
app/models/group.rb

@@ -6,10 +6,10 @@ class Group < ApplicationModel
   include ChecksLatestChangeObserved
   include HasHistory
 
-  has_and_belongs_to_many  :users,         after_add: :cache_update, after_remove: :cache_update
-  belongs_to               :email_address
-  belongs_to               :signature
-  validates                :name, presence: true
+  belongs_to :email_address
+  belongs_to :signature
+
+  validates :name, presence: true
 
   activity_stream_permission 'admin.group'
 end

+ 5 - 8
app/models/role_group.rb

@@ -1,13 +1,10 @@
 # Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/
 
 class RoleGroup < ApplicationModel
-  self.table_name   = 'roles_groups'
-  self.primary_keys = :role_id, :group_id, :access
-  belongs_to          :role
-  belongs_to          :group
-  validates           :access, presence: true
+  include HasGroupRelationDefinition
 
-  def self.ref_key
-    :role_id
-  end
+  self.table_name = 'roles_groups'
+
+  # don't list roles in Group association result
+  Group.association_attributes_ignored :roles
 end

+ 2 - 36
app/models/user_group.rb

@@ -1,41 +1,7 @@
 # Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/
 
 class UserGroup < ApplicationModel
-  self.table_name   = 'groups_users'
-  self.primary_keys = :user_id, :group_id, :access
-  belongs_to          :user
-  belongs_to          :group
-  validates           :access, presence: true
+  include HasGroupRelationDefinition
 
-  def self.ref_key
-    :user_id
-  end
-
-  def cache_update
-    group.cache_update(nil)
-    user.cache_update(nil)
-    super
-  end
-
-  def cache_delete
-    group.cache_update(nil)
-    user.cache_update(nil) if user.present?
-    super
-  end
-
-  private
-
-  def validate_access
-    query = self.class.where(group: group, user: user)
-
-    query = if access == 'full'
-              query.where.not(access: 'full')
-            else
-              query.where(access: 'full')
-            end
-
-    errors.add(:access, 'User can have full or granular access to group') if query.exists?
-  end
-
-  validate :validate_access
+  self.table_name = 'groups_users'
 end

+ 62 - 0
spec/models/concerns/has_group_relation_definition_examples.rb

@@ -0,0 +1,62 @@
+# Requires: let!(:group_relation_instance) { ... }
+RSpec.shared_examples 'HasGroupRelationDefinition' do
+
+  let(:group_relation_model_key) { group_relation_instance.model_name.element }
+
+  context 'relation creation' do
+
+    it 'refreshes updated_at of related instances' do
+      group = create(:group)
+
+      travel 1.minute
+
+      expect do
+        described_class.create!(
+          group_relation_model_key => group_relation_instance,
+          group: group
+        )
+      end.to change {
+        group.reload.updated_at
+      }.and change {
+        group_relation_instance.reload.updated_at
+      }
+    end
+  end
+
+  context 'related instance deletion' do
+
+    it 'refreshes updated_at of group instance' do
+      group = create(:group)
+
+      described_class.create!(
+        group_relation_model_key => group_relation_instance,
+        group: group
+      )
+
+      travel 1.minute
+
+      expect do
+        group.destroy
+      end.to change {
+        group_relation_instance.reload.updated_at
+      }
+    end
+
+    it 'refreshes updated_at of relation instance' do
+      group = create(:group)
+
+      described_class.create!(
+        group_relation_model_key => group_relation_instance,
+        group: group
+      )
+
+      travel 1.minute
+
+      expect do
+        group_relation_instance.destroy
+      end.to change {
+        group.reload.updated_at
+      }
+    end
+  end
+end

+ 18 - 0
spec/models/concerns/has_groups_examples.rb

@@ -242,6 +242,24 @@ RSpec.shared_examples 'HasGroups' do
             described_class.group_through.klass.count
           }.by(-3)
         end
+
+        it 'prevents having full and other privilege at the same time' do
+
+          invalid_combination = %w[full read change]
+          exception           = ActiveRecord::RecordInvalid
+
+          expect do
+            group_access_instance.group_names_access_map = {
+              group_full.name => invalid_combination,
+            }
+          end.to raise_error(exception)
+
+          expect do
+            group_access_instance.group_names_access_map = {
+              group_full.name => invalid_combination.reverse,
+            }
+          end.to raise_error(exception)
+        end
       end
 
       context 'new instance' do

+ 21 - 0
spec/models/role_group_spec.rb

@@ -0,0 +1,21 @@
+require 'rails_helper'
+require 'models/concerns/has_group_relation_definition_examples'
+
+RSpec.describe RoleGroup do
+
+  let!(:group_relation_instance) { create(:role) }
+
+  include_examples 'HasGroupRelationDefinition'
+
+  it 'prevents roles from beeing in Group assets' do
+
+    group = create(:group)
+
+    described_class.create!(
+      group: group,
+      role:  create(:role)
+    )
+    expect(group.assets({})[:Group][group.id]).not_to include('role_ids')
+  end
+
+end

+ 9 - 0
spec/models/user_group_spec.rb

@@ -0,0 +1,9 @@
+require 'rails_helper'
+require 'models/concerns/has_group_relation_definition_examples'
+
+RSpec.describe UserGroup do
+
+  let!(:group_relation_instance) { create(:agent_user) }
+
+  include_examples 'HasGroupRelationDefinition'
+end

+ 0 - 44
test/unit/user_group_test.rb

@@ -1,44 +0,0 @@
-require 'test_helper'
-
-class UserGroupTest < ActiveSupport::TestCase
-  test 'user group permissions' do
-    rand = rand(9_999_999_999)
-    agent1 = User.create!(
-      login: "agent-permission-check#{rand}@example.com",
-      firstname: 'vaild_agent_group_permission-1',
-      lastname: 'Agent',
-      email: "agent-permission-check#{rand}@example.com",
-      password: 'agentpw',
-      active: true,
-      roles: Role.where(name: 'Agent'),
-      groups: Group.all,
-      updated_by_id: 1,
-      created_by_id: 1,
-    )
-
-    group1 = Group.create!(
-      name: "GroupPermissionsTest-#{rand(9_999_999_999)}",
-      active: true,
-      updated_by_id: 1,
-      created_by_id: 1,
-    )
-
-    assert_nothing_raised do
-      UserGroup.create!(user: agent1, group: group1, access: 'full')
-    end
-
-    assert_raises do
-      UserGroup.create!(user: agent1, group: group1, access: 'read')
-    end
-
-    UserGroup.where(user: agent1, group: group1).destroy_all
-
-    assert_nothing_raised do
-      UserGroup.create!(user: agent1, group: group1, access: 'read')
-    end
-
-    assert_raises do
-      UserGroup.create!(user: agent1, group: group1, access: 'full')
-    end
-  end
-end