diff --git a/app/models/miq_product_feature.rb b/app/models/miq_product_feature.rb index b6e2a8a81fe..7b2d1b32ed3 100644 --- a/app/models/miq_product_feature.rb +++ b/app/models/miq_product_feature.rb @@ -9,6 +9,7 @@ class MiqProductFeature < ApplicationRecord has_and_belongs_to_many :miq_user_roles, :join_table => :miq_roles_features has_many :miq_product_features_shares has_many :shares, :through => :miq_product_features_shares + belongs_to :tenant virtual_delegate :identifier, :to => :parent, :prefix => true, :allow_nil => true @@ -22,13 +23,37 @@ class MiqProductFeature < ApplicationRecord :description, :feature_type, :hidden, - :protected + :protected, + :tenant_id ] - FEATURE_TYPE_ORDER = ["view", "control", "admin", "node"] + FEATURE_TYPE_ORDER = %w(view control admin node).freeze REQUIRED_ATTRIBUTES = [:identifier].freeze - OPTIONAL_ATTRIBUTES = [:name, :feature_type, :description, :children, :hidden, :protected].freeze + OPTIONAL_ATTRIBUTES = %i(name feature_type description children hidden protected).freeze ALLOWED_ATTRIBUTES = (REQUIRED_ATTRIBUTES + OPTIONAL_ATTRIBUTES).freeze + TENANT_FEATURE_ROOT_IDENTIFIERS = %w(dialog_new_editor dialog_edit_editor dialog_copy_editor dialog_delete).freeze + + def name + value = self[:name] + self[:tenant_id] ? "#{value} (#{tenant.name})" : value + end + + def description + value = self[:description] + self[:tenant_id] ? "#{value} for tenant #{tenant.name}" : value + end + + def self.tenant_identifier(identifier, tenant_id) + "#{identifier}_tenant_#{tenant_id}" + end + + def self.root_tenant_identifier?(identifier) + TENANT_FEATURE_ROOT_IDENTIFIERS.include?(identifier) + end + + def self.current_tenant_identifier(identifier) + identifier && feature_details(identifier) && root_tenant_identifier?(identifier) ? tenant_identifier(identifier, User.current_tenant.id) : identifier + end def self.feature_yaml(path = FIXTURE_PATH) "#{path}.yml".freeze @@ -74,6 +99,12 @@ def self.feature_exists?(ident) features.key?(ident) end + def self.invalidate_caches + @feature_cache = nil + @obj_cache = nil + @detail = nil + end + def self.features @feature_cache ||= begin # create hash with parent identifier and details @@ -106,6 +137,18 @@ def self.seed seed_features end + def self.with_tenant_feature_root_features + where(:identifier => TENANT_FEATURE_ROOT_IDENTIFIERS) + end + + def self.seed_tenant_miq_product_features + result = with_tenant_feature_root_features.map.each do |tenant_miq_product_feature| + Tenant.all.map { |tenant| tenant.build_miq_product_feature(tenant_miq_product_feature) } + end.flatten + + MiqProductFeature.create(result).map(&:identifier) + end + def self.seed_features(path = FIXTURE_PATH) fixture_yaml = feature_yaml(path) @@ -117,7 +160,8 @@ def self.seed_features(path = FIXTURE_PATH) seed_from_hash(YAML.load_file(fixture), seen, root_feature) end - deletes = where.not(:identifier => seen.values.flatten).destroy_all + tenant_identifiers = seed_tenant_miq_product_features + deletes = where.not(:identifier => seen.values.flatten + tenant_identifiers).destroy_all _log.info("Deleting product features: #{deletes.collect(&:identifier).inspect}") unless deletes.empty? seen end diff --git a/app/models/tenant.rb b/app/models/tenant.rb index 965687e24ab..b1221dd54f1 100644 --- a/app/models/tenant.rb +++ b/app/models/tenant.rb @@ -36,6 +36,7 @@ class Tenant < ApplicationRecord has_many :services, :dependent => :destroy has_many :shares has_many :authentications, :dependent => :nullify + has_many :miq_product_features, :dependent => :destroy belongs_to :default_miq_group, :class_name => "MiqGroup", :dependent => :destroy belongs_to :source, :polymorphic => true @@ -57,7 +58,7 @@ class Tenant < ApplicationRecord virtual_column :display_type, :type => :string before_save :nil_blanks - after_create :create_tenant_group + after_create :create_tenant_group, :create_miq_product_features_for_tenant_nodes before_destroy :ensure_can_be_destroyed def self.scope_by_tenant? @@ -290,6 +291,30 @@ def allowed? Rbac::Filterer.filtered_object(self).present? end + def create_miq_product_features_for_tenant_nodes + result = MiqProductFeature.with_tenant_feature_root_features.map.each do |tenant_miq_product_feature| + build_miq_product_feature(tenant_miq_product_feature) + end.flatten + + result = MiqProductFeature.create(result).map(&:identifier) + + MiqProductFeature.invalidate_caches + result + end + + def build_miq_product_feature(miq_product_feature) + attrs = { + :name => miq_product_feature.name, :description => miq_product_feature.description, + :feature_type => 'admin', + :hidden => false, + :identifier => MiqProductFeature.tenant_identifier(miq_product_feature.identifier, id), + :tenant_id => id, + :parent_id => miq_product_feature.id + } + + attrs + end + private # when a root tenant has an attribute with a nil value, diff --git a/db/fixtures/miq_product_features.yml b/db/fixtures/miq_product_features.yml index 19899854fa2..f2509a768ee 100644 --- a/db/fixtures/miq_product_features.yml +++ b/db/fixtures/miq_product_features.yml @@ -2440,34 +2440,34 @@ - :name: Dialogs :description: Dialogs Accordion :feature_type: node - :hidden: true + :hidden: false :identifier: dialog_accord :children: - :name: Modify :description: Modify Dialogs :feature_type: admin :identifier: dialog_admin - :hidden: true + :hidden: false :children: - :name: Add :description: Add Dialog in the Dialog Editor :feature_type: admin - :hidden: true + :hidden: false :identifier: dialog_new_editor - :name: Edit :description: Edit Dialog in the Dialog Editor :feature_type: admin - :hidden: true + :hidden: false :identifier: dialog_edit_editor - :name: Copy :description: Copy Dialog :feature_type: admin - :hidden: true + :hidden: false :identifier: dialog_copy_editor - :name: Delete :description: Delete Dialog :feature_type: admin - :hidden: true + :hidden: false :identifier: dialog_delete - :name: Provisioning Dialogs :description: Provisioning Dialogs Accordion diff --git a/lib/rbac/authorizer.rb b/lib/rbac/authorizer.rb index 01f2d964772..06c8950175f 100644 --- a/lib/rbac/authorizer.rb +++ b/lib/rbac/authorizer.rb @@ -22,6 +22,8 @@ def role_allows?(options = {}) any = options[:any] + identifier = MiqProductFeature.current_tenant_identifier(identifier) + auth = if any.present? user_role_allows_any?(user, :identifiers => (identifiers || [identifier])) else diff --git a/spec/models/miq_product_feature_spec.rb b/spec/models/miq_product_feature_spec.rb index d7a66825108..5937b234eea 100644 --- a/spec/models/miq_product_feature_spec.rb +++ b/spec/models/miq_product_feature_spec.rb @@ -2,6 +2,18 @@ require 'pathname' describe MiqProductFeature do + let(:miq_product_feature_class) do + Class.new(described_class) do + def self.with_parent_tenant_nodes + includes(:parent).where(:parents_miq_product_features => {:identifier => self::TENANT_FEATURE_ROOT_IDENTIFIERS}) + end + + def self.tenant_features_in_hash + with_parent_tenant_nodes.map { |x| x.slice(:name, :description, :identifier, :tenant_id) } + end + end + end + # - container_dashboard # - miq_report_widget_editor # - miq_report_widget_admin @@ -55,7 +67,7 @@ def traverse_product_features(product_feature, &block) :children => [ { :feature_type => "node", - :identifier => "one", + :identifier => "dialog_new_editor", :name => "One", :children => [] } @@ -77,7 +89,7 @@ def traverse_product_features(product_feature, &block) it "existing records" do deleted = FactoryGirl.create(:miq_product_feature, :identifier => "xxx") - changed = FactoryGirl.create(:miq_product_feature, :identifier => "one", :name => "XXX") + changed = FactoryGirl.create(:miq_product_feature, :identifier => "dialog_new_editor", :name => "XXX") unchanged = FactoryGirl.create(:miq_product_feature_everything) unchanged_orig_updated_at = unchanged.updated_at @@ -90,7 +102,7 @@ def traverse_product_features(product_feature, &block) it "additional yaml feature" do additional = { :feature_type => "node", - :identifier => "two", + :identifier => "dialog_edit_editor", :children => [] } @@ -101,11 +113,137 @@ def traverse_product_features(product_feature, &block) status_seed = MiqProductFeature.seed_features(feature_path) expect(MiqProductFeature.count).to eq(3) - expect(status_seed[:created]).to match_array %w(everything one two) + expect(status_seed[:created]).to match_array %w(everything dialog_new_editor dialog_edit_editor) additional_file.unlink Dir.rmdir(feature_path) end + + context 'dynamic product features' do + context 'add new' do + let(:base) do + { + :feature_type => "node", + :identifier => "everything", + :children => [ + { + :feature_type => "node", + :identifier => "one", + :name => "One", + :children => [ + { + :feature_type => "admin", + :identifier => "dialog_copy_editor", + :name => "Edit", + :description => "XXX" + } + ] + } + ] + } + end + + let(:root_tenant) do + Tenant.seed + Tenant.default_tenant + end + + let!(:tenant) { FactoryGirl.create(:tenant, :parent => root_tenant) } + + before do + MiqProductFeature.seed_features(feature_path) + end + + it "creates tenant features" do + features = miq_product_feature_class.tenant_features_in_hash + expect(features).to match_array([{ "name" => "Edit (#{root_tenant.name})", "description" => "XXX for tenant #{root_tenant.name}", + "identifier" => "dialog_copy_editor_tenant_#{root_tenant.id}", "tenant_id" => root_tenant.id}, + {"name" => "Edit (#{tenant.name})", "description" => "XXX for tenant #{tenant.name}", + "identifier" => "dialog_copy_editor_tenant_#{tenant.id}", "tenant_id" => tenant.id}]) + + expect(MiqProductFeature.where(:identifier => "dialog_copy_editor", :name => "Edit").count).to eq(1) + end + + context "add tenant node product features" do + let(:base) do + { + :feature_type => "node", + :identifier => "everything", + :children => [ + { + :feature_type => "node", + :identifier => "one", + :name => "One", + :children => [ + { + :feature_type => "admin", + :identifier => "dialog_copy_editor", + :name => "Edit", + :description => "XXX" + } + ] + }, + { + :feature_type => "admin", + :identifier => "dialog_delete", + :name => "Add", + :description => "YYY" + } + ] + } + end + + it "add new tenant feature" do + features = miq_product_feature_class.tenant_features_in_hash + expect(features).to match_array([{ "name" => "Edit (#{root_tenant.name})", "description" => "XXX for tenant #{root_tenant.name}", + "identifier" => "dialog_copy_editor_tenant_#{root_tenant.id}", "tenant_id" => root_tenant.id}, + {"name" => "Edit (#{tenant.name})", "description" => "XXX for tenant #{tenant.name}", + "identifier" => "dialog_copy_editor_tenant_#{tenant.id}", "tenant_id" => tenant.id}, + {"name" => "Add (#{root_tenant.name})", "description" => "YYY for tenant #{root_tenant.name}", + "identifier" => "dialog_delete_tenant_#{root_tenant.id}", "tenant_id" => root_tenant.id}, + {"name" => "Add (#{tenant.name})", "description" => "YYY for tenant #{tenant.name}", + "identifier" => "dialog_delete_tenant_#{tenant.id}", "tenant_id" => tenant.id}]) + + expect(MiqProductFeature.where(:identifier => "dialog_delete", :name => "Add").count).to eq(1) + end + + context "remove added tenant feaure" do + let(:base) do + { + :feature_type => "node", + :identifier => "everything", + :children => [ + { + :feature_type => "node", + :identifier => "one", + :name => "One", + :children => [ + { + :feature_type => "admin", + :identifier => "dialog_copy_editor", + :name => "Edit", + :description => "XXX" + } + ] + } + ] + } + end + + it "removes tenant features" do + features = miq_product_feature_class.tenant_features_in_hash + + expect(features).to match_array([{ "name" => "Edit (#{root_tenant.name})", "description" => "XXX for tenant #{root_tenant.name}", + "identifier" => "dialog_copy_editor_tenant_#{root_tenant.id}", "tenant_id" => root_tenant.id}, + {"name" => "Edit (#{tenant.name})", "description" => "XXX for tenant #{tenant.name}", + "identifier" => "dialog_copy_editor_tenant_#{tenant.id}", "tenant_id" => tenant.id}]) + + expect(MiqProductFeature.where(:identifier => "dialog_copy_editor", :name => "Edit").count).to eq(1) + end + end + end + end + end end describe '#feature_children' do diff --git a/spec/models/miq_user_role_spec.rb b/spec/models/miq_user_role_spec.rb index 70a6cc83759..8e0f3146254 100644 --- a/spec/models/miq_user_role_spec.rb +++ b/spec/models/miq_user_role_spec.rb @@ -52,6 +52,7 @@ host_show_list policy vm + dialog_edit_editor )) feature1 = MiqProductFeature.find_all_by_identifier("dashboard_admin") @@ -70,6 +71,35 @@ @user3 = FactoryGirl.create(:user, :userid => "user3", :miq_groups => [@group3]) end + context "dynamic tenant product features" do + let(:root_tenant) do + Tenant.seed + Tenant.default_tenant + end + + let!(:tenant_1) { FactoryGirl.create(:tenant, :parent => root_tenant) } + let!(:tenant_2) { FactoryGirl.create(:tenant, :parent => root_tenant) } + + let(:feature) { MiqProductFeature.find_all_by_identifier(["dialog_edit_editor_tenant_#{tenant_2.id}"]) } + let(:role) { FactoryGirl.create(:miq_user_role, :miq_product_features => feature) } + let(:group_tenant_1) { FactoryGirl.create(:miq_group, :miq_user_role => role, :tenant => tenant_1) } + let(:group_tenant_2) { FactoryGirl.create(:miq_group, :miq_user_role => role, :tenant => tenant_2) } + let!(:user_1) { FactoryGirl.create(:user, :userid => "user_1", :miq_groups => [group_tenant_1]) } + let!(:user_2) { FactoryGirl.create(:user, :userid => "user_2", :miq_groups => [group_tenant_2]) } + + it "doesn't authorise user without dynamic product feature" do + User.with_user(user_1) do + expect(user_1.role_allows?(:identifier => "dialog_edit_editor")).to be_falsey + end + end + + it "authorise user with dynamic product feature" do + User.with_user(user_2) do + expect(user_2.role_allows?(:identifier => "dialog_edit_editor")).to be_truthy + end + end + end + it "should return the correct answer calling allows? when requested feature is directly assigned or a descendant of a feature in a role" do expect(@role1.allows?(:identifier => "dashboard_admin")).to eq(true) expect(@role1.allows?(:identifier => "dashboard_add")).to eq(true) diff --git a/spec/models/tenant_spec.rb b/spec/models/tenant_spec.rb index 2a8690cee50..37d2ed601f5 100644 --- a/spec/models/tenant_spec.rb +++ b/spec/models/tenant_spec.rb @@ -850,5 +850,37 @@ expect(tenant.default_miq_group).not_to eq(false_group) expect(tenant.default_miq_group).to be_tenant_group end + + context 'dynamic product features' do + let!(:miq_product_feature_1) { FactoryGirl.create(:miq_product_feature, :name => "Edit1", :description => "XXX1", :identifier => 'dialog_edit_editor') } + let!(:miq_product_feature_2) { FactoryGirl.create(:miq_product_feature, :name => "Edit2", :description => "XXX2", :identifier => 'dialog_new_editor') } + + let(:tenant_product_feature) { FactoryGirl.create(:tenant) } + + it "properly creates a related product feature" do + features = tenant_product_feature.miq_product_features.map { |x| x.slice(:name, :description, :identifier, :feature_type) } + expect(features).to match_array([{"name" => "#{miq_product_feature_1.name} (#{tenant_product_feature.name})", "description" => "#{miq_product_feature_1.description} for tenant #{tenant_product_feature.name}", + "identifier" => "dialog_edit_editor_tenant_#{tenant_product_feature.id}", "feature_type" => "admin"}, + {"name" => "#{miq_product_feature_2.name} (#{tenant_product_feature.name})", + "description" => "#{miq_product_feature_2.description} for tenant #{tenant_product_feature.name}", + "identifier" => "dialog_new_editor_tenant_#{tenant_product_feature.id}", "feature_type" => "admin"}]) + end + + describe "#create_miq_product_features_for_tenant_nodes" do + let(:tenant_product_feature) { FactoryGirl.create(:tenant) } + + it "creates product features for tenant nodes" do + expect(tenant_product_feature.create_miq_product_features_for_tenant_nodes).to match_array(["dialog_edit_editor_tenant_#{tenant_product_feature.id}", "dialog_new_editor_tenant_#{tenant_product_feature.id}"]) + end + end + + describe "#destroy_tenant_feature_for_tenant_node" do + it "destroys product features" do + tenant_product_feature.destroy + + expect(MiqProductFeature.where(:identifier => ["dialog_edit_editor_tenant_#{tenant_product_feature.id}", "ab_group_admin_tenant_#{tenant_product_feature.id}"], :feature_type => 'tenant').count).to be_zero + end + end + end end end diff --git a/spec/support/evm_spec_helper.rb b/spec/support/evm_spec_helper.rb index 7848f8ec4c3..86e59ad0b8c 100644 --- a/spec/support/evm_spec_helper.rb +++ b/spec/support/evm_spec_helper.rb @@ -99,6 +99,7 @@ def self.seed_specific_product_features(*features) hashes = YAML.load_file(MiqProductFeature.feature_yaml) filtered = filter_specific_features([hashes], features).first MiqProductFeature.seed_from_hash(filtered) + MiqProductFeature.seed_tenant_miq_product_features end def self.filter_specific_features(hashes, features)