Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dynamic product features according to tenants #18102

Merged
merged 2 commits into from
Oct 30, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 48 additions & 4 deletions app/models/miq_product_feature.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)

Expand All @@ -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
Expand Down
27 changes: 26 additions & 1 deletion app/models/tenant.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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?
Expand Down Expand Up @@ -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,
Expand Down
12 changes: 6 additions & 6 deletions db/fixtures/miq_product_features.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions lib/rbac/authorizer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
146 changes: 142 additions & 4 deletions spec/models/miq_product_feature_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -55,7 +67,7 @@ def traverse_product_features(product_feature, &block)
:children => [
{
:feature_type => "node",
:identifier => "one",
:identifier => "dialog_new_editor",
:name => "One",
:children => []
}
Expand All @@ -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

Expand All @@ -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 => []
}

Expand All @@ -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
Expand Down
Loading