Last active
December 28, 2015 01:19
-
-
Save kporangehat/7419853 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Takes 2 PermissionRuleSet code values and does a comparison of all | |
# entity-level and field-level rules. | |
# | |
# The diff is calculated by running PermissionRuleSet.allow? on each | |
# type of rule and storing the result in a hash for that ruleset. Then | |
# a diff is run on the two tables to generate the result. | |
# | |
# If a field is missing on the current instance, the call to allow? should | |
# still return the correct result as if the field existed. Missing fields | |
# are designated with an * before the field name. The ruleset that references | |
# the missing field is contained in ()'s after the field name. | |
# | |
# NOTE: Missing fields can be caused by entity types not being enabled. For | |
# example, Shotgun will automatically add certain DisplayColumns for | |
# entity types when they are enabled. | |
# | |
# RULE TYPES: The following rule types are checked: | |
# create_entity | |
# retire_entity | |
# see_entity | |
# see_field | |
# update_field | |
# Conditional rules and field rules with value options are not compared | |
# since they are value dependent. Special admin-type rules are also not | |
# compared. | |
# | |
# Examples: | |
# > # prints differences to the console | |
# > diff = PermissionDiff.new | |
# > diff.compare('artist', 'manager') | |
# > diff.pp | |
# =========================================================================== | |
# Rule differences [artist, artist_int] | |
# *missing fields on current instance (ruleset) | |
# =========================================================================== | |
# | |
# Asset | |
# ---------------------------------------- | |
# field Rules | |
# sg_status_list: {"update_field"=>[false, true]} | |
# *sg_site_cdn_status (artist_int): {"update_field"=>[false, true]} | |
# | |
# Attachment | |
# ---------------------------------------- | |
# field Rules | |
# sg_status_list: { "update_field"=>[false, true]} | |
# | |
# Booking | |
# ---------------------------------------- | |
# see_entity: [true, false] | |
# ... | |
# ... | |
# | |
# | |
# | |
# > # outputs differences to a csv file in the directory TMPDIR | |
# > diff = PermissionDiff.new | |
# > diff.compare('artist', 'manager') | |
# > diff.csv | |
# Wrote CSV file to /var/tmp/PermissionsDiff_artist_manager.csv | |
# location of csv files | |
TMPDIR = '/var/tmp' | |
ENTITY_TYPES = ShotgunConfig.instance['entity.system_types'].sort | |
DCS_TO_IGNORE = ['cached_display_name'] | |
# borrowed from https://gist.github.com/henrik/146844 | |
# for doing the diff of the 2 ruleset allow tables | |
class Hash | |
def deep_diff(b) | |
a = self | |
(a.keys | b.keys).inject({}) do |diff, k| | |
if a[k] != b[k] | |
if a[k].respond_to?(:deep_diff) && b[k].respond_to?(:deep_diff) | |
diff[k] = a[k].deep_diff(b[k]) | |
else | |
diff[k] = [a[k], b[k]] | |
end | |
end | |
diff | |
end | |
end | |
end | |
class PermissionsDiff | |
attr_reader :ruleset_a | |
attr_reader :ruleset_b | |
attr_reader :table_a | |
attr_reader :table_b | |
attr_reader :results | |
attr_reader :missing_fields | |
def initialize() | |
@ruleset_a = {} | |
@ruleset_b = {} | |
@table_a = {} | |
@table_b = {} | |
@results = {} | |
@missing_fields = {} | |
end | |
# Load up the PermissionRuleSet from Shotgun | |
# | |
def load_permission_rule_set(ruleset_code) | |
rs = PermissionRuleSet.find_by_code(ruleset_code) | |
if rs.nil? | |
raise "The PermissionRuleSet '#{ruleset_code}' does not exist!" | |
end | |
return rs | |
end | |
# Iterate through PermissionRuleSet.permission_rules and return an array | |
# of any field rules for entity_type that reference a non-existent field | |
# on the current Shtogun instance | |
# | |
def get_rules_with_missing_fields(ruleset, entity_type) | |
missing_field_rules = [] | |
entity_type_fields = DisplayColumnRegistry.instance.columns_for_class(entity_type).keys | |
ruleset.permission_rules.each do |r| | |
next if r.parameter_1 != entity_type | |
if !r.parameter_2.nil? and !entity_type_fields.include?(r.parameter_2) | |
missing_field_rules << r | |
end | |
end | |
missing_field_rules | |
end | |
# Return table values as a hash declaring whether the specified ruleset allows | |
# see_field and update_field for the specified entity_type and field_name. | |
# If the DisplayColumn is read_only or create_only, then that overrides any | |
# permissions. | |
def get_field_rules(ruleset, entity_type, field_name, dc_override=nil) | |
rules = { | |
'see_field' => ruleset.allow?(:see_field, {:entity_type=>entity_type, :field_name=>field_name}) | |
} | |
if dc_override.nil? | |
rules['update_field'] = ruleset.allow?(:update_field, {:entity_type=>entity_type, :field_name=>field_name, :field_value=>nil}) | |
else | |
rules['update_field'] = dc_override | |
end | |
rules | |
end | |
# Lookup all fields for entity_type and return a hash representing whether | |
# rules are allowed for each field. Note that this does not cover any rules | |
# that are referencing missing fields since they aren't returned by | |
# DisplayColumnRegistry. We cover that later with add_missing_field_rules_to_table() | |
# | |
def get_entity_field_rules(ruleset, entity_type) | |
return {} if entity_type.nil? | |
rule_table = {} | |
entity_type_dcs = DisplayColumnRegistry.instance.columns_for_class(entity_type) | |
entity_type_dcs.each do |field_name, dc| | |
next if DCS_TO_IGNORE.include?(field_name) | |
dc_override = nil | |
if dc.read_only == true | |
dc_override = 'read_only' | |
elsif dc.create_only == true | |
dc_override = 'create_only' | |
end | |
rule_table[field_name] = get_field_rules(ruleset, entity_type, field_name, dc_override) | |
end | |
rule_table | |
end | |
# Return hash table containing all of the entity-level and field-level rule | |
# allow? values for ruleset. | |
# | |
def get_entity_rules(ruleset, entity_type=nil) | |
rule_table = { | |
'see_entity' => ruleset.allow?(:see_entity, {:entity_type=>entity_type}), | |
'create_entity' => ruleset.allow?(:create_entity, {:entity_type=>entity_type}), | |
'retire_entity' => ruleset.allow?(:retire_entity, {:entity_type=>entity_type}) | |
} | |
field_rules = {'fields' => get_entity_field_rules(ruleset, entity_type)} | |
rule_table.merge!(field_rules) | |
rule_table | |
end | |
# Return complete hash table representing all of the entity-level and | |
# field-level rule allow? values for ruleset. Include blanket rules that | |
# have no entity_type and assigned them with the 'ALL' key. | |
# | |
# Store any field-level rules that reference a non-existent field a | |
# separate key | |
def build_rule_table(ruleset) | |
table = {} | |
table['ALL'] = get_entity_rules(ruleset, nil) | |
table['ALL']['missing_fields'] = [] | |
ENTITY_TYPES.each do |et| | |
table[et] = get_entity_rules(ruleset, et) | |
table[et]['missing_fields'] = get_rules_with_missing_fields(ruleset, et) | |
end | |
table | |
end | |
# Add any missing field rules to the existing tables since they didn't get | |
# run previously. Even if the fieldname doesn't exists on the server, | |
# ruleset.allow? should return the extected result. Delete the missing_fields | |
# key in the tables to clean things up. | |
# | |
def add_missing_field_rules_to_tables() | |
[@table_a, @table_b].each do |table| | |
table.each do |et, info| | |
info['missing_fields'].each do |r| | |
@table_a[et]['fields']["*#{r.parameter_2} (#{r.permission_rule_set.code})"] = get_field_rules(@ruleset_a, et, r.parameter_2) | |
@table_b[et]['fields']["*#{r.parameter_2} (#{r.permission_rule_set.code})"] = get_field_rules(@ruleset_b, et, r.parameter_2) | |
end | |
info['missing_fields'].each do |r| | |
rs_code = r.permission_rule_set.code | |
@missing_fields[rs_code] << "#{et}.#{r.parameter_2}" | |
end | |
info.delete('missing_fields') | |
end | |
end | |
end | |
# Loads two rulesets by code, populates hash table tracking whether | |
# each rule type is allowed or not. Generates a diff table with the | |
# differences between the two ruleset allow tables. | |
# | |
def compare(rs_code_a, rs_code_b) | |
@ruleset_a = load_permission_rule_set(rs_code_a) | |
@ruleset_b = load_permission_rule_set(rs_code_b) | |
@missing_fields = {rs_code_a=>[], rs_code_b=>[]} | |
@table_a = build_rule_table(@ruleset_a) | |
@table_b = build_rule_table(@ruleset_b) | |
add_missing_field_rules_to_tables() | |
@results = @table_a.deep_diff(@table_b) | |
nil | |
end | |
# Prints results of diff comparison of the two rulesets to the console. | |
# | |
def pp() | |
puts "="*80 | |
puts " Rule differences [#{@ruleset_a.code}, #{@ruleset_b.code}] " | |
puts " *missing fields on current instance (ruleset)" | |
puts "="*80 | |
@results.each do |et, info| | |
puts | |
puts et | |
puts "-"*40 | |
info.each do |rule_type, e_diffs| | |
if rule_type == 'fields' | |
puts " field Rules" | |
e_diffs.each do |field, f_diffs| | |
puts "\t #{field}: #{f_diffs}" | |
end | |
else | |
puts " #{rule_type}: #{e_diffs}" | |
end | |
end | |
end | |
puts | |
end | |
# Writes a CSV file with diff results between two rulesets. This is useful | |
# for delivering reports to clients. | |
# | |
def csv() | |
require 'csv' | |
filename = "PermissionsDiff_#{@ruleset_a.code}_#{@ruleset_b.code}.csv" | |
CSV.open("#{TMPDIR}/#{filename}", "wb") do |csv| | |
# csv_data = CSV.generate do |csv| | |
csv << ['ENTITY TYPE', 'FIELD', 'RULE', @ruleset_a.code, @ruleset_b.code] | |
@results.each do |et, info| | |
info.each do |rule_type, e_diffs| | |
if rule_type == 'fields' | |
e_diffs.each do |field, f_diffs| | |
f_diffs.each do |rule, allow| | |
csv << [et, field, rule, allow[0], allow[1]] | |
end | |
end | |
else | |
csv << [et, nil, rule_type, e_diffs[0], e_diffs[1]] | |
end | |
end | |
end | |
# end | |
end | |
puts "Wrote CSV file to #{TMPDIR}/#{filename}" | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment