cantango 0.9.3.2 → 0.9.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (80) hide show
  1. data/README.textile +11 -9
  2. data/VERSION +1 -1
  3. data/cantango.gemspec +24 -3
  4. data/lib/cantango/ability/cache/key.rb +6 -2
  5. data/lib/cantango/ability/cache/reader.rb +3 -0
  6. data/lib/cantango/ability/cache/session_cache.rb +7 -3
  7. data/lib/cantango/ability/cache/writer.rb +8 -2
  8. data/lib/cantango/ability/cache.rb +25 -8
  9. data/lib/cantango/ability/cache_helpers.rb +4 -13
  10. data/lib/cantango/ability/cached_executor.rb +0 -0
  11. data/lib/cantango/ability/engine_helpers.rb +4 -1
  12. data/lib/cantango/ability/executor.rb +67 -0
  13. data/lib/cantango/ability/permission_helpers.rb +0 -1
  14. data/lib/cantango/ability.rb +1 -1
  15. data/lib/cantango/cached_ability.rb +3 -2
  16. data/lib/cantango/configuration/engines/cache.rb +0 -3
  17. data/lib/cantango/configuration/engines/engine.rb +5 -0
  18. data/lib/cantango/configuration/engines/permission.rb +5 -4
  19. data/lib/cantango/configuration/engines/permit.rb +0 -5
  20. data/lib/cantango/configuration/engines/user_ac.rb +6 -3
  21. data/lib/cantango/configuration/models/active_record.rb +11 -0
  22. data/lib/cantango/configuration/models/data_mapper.rb +12 -0
  23. data/lib/cantango/configuration/models/generic.rb +12 -0
  24. data/lib/cantango/configuration/models/mongo.rb +12 -0
  25. data/lib/cantango/configuration/models/mongo_mapper.rb +11 -0
  26. data/lib/cantango/configuration/models/mongoid.rb +13 -0
  27. data/lib/cantango/configuration/models.rb +27 -2
  28. data/lib/cantango/configuration/permits.rb +2 -1
  29. data/lib/cantango/configuration.rb +14 -0
  30. data/lib/cantango/engine.rb +5 -19
  31. data/lib/cantango/model/scope.rb +19 -5
  32. data/lib/cantango/permission_engine/collector.rb +3 -0
  33. data/lib/cantango/permission_engine/evaluator.rb +5 -0
  34. data/lib/cantango/permission_engine/factory.rb +3 -0
  35. data/lib/cantango/permission_engine/loader/permissions.rb +7 -8
  36. data/lib/cantango/permission_engine/store.rb +0 -1
  37. data/lib/cantango/permission_engine/yaml_store.rb +15 -4
  38. data/lib/cantango/permission_engine.rb +21 -4
  39. data/lib/cantango/permit_engine/factory.rb +10 -4
  40. data/lib/cantango/permit_engine.rb +39 -9
  41. data/lib/cantango/permits/account_permit/builder.rb +6 -2
  42. data/lib/cantango/{user_ac_engine → permits}/executor.rb +28 -30
  43. data/lib/cantango/permits/permit/class_methods.rb +21 -0
  44. data/lib/cantango/permits/permit/execute.rb +81 -0
  45. data/lib/cantango/permits/permit/license.rb +26 -0
  46. data/lib/cantango/permits/permit.rb +19 -138
  47. data/lib/cantango/permits/role_group_permit/builder.rb +5 -1
  48. data/lib/cantango/permits/role_group_permit.rb +3 -3
  49. data/lib/cantango/permits/role_permit/builder.rb +4 -0
  50. data/lib/cantango/permits/user_permit/builder.rb +5 -1
  51. data/lib/cantango/permits/user_permit.rb +1 -1
  52. data/lib/cantango/permits.rb +1 -0
  53. data/lib/cantango/rails/engine.rb +0 -3
  54. data/lib/cantango/rails/helpers/base_helper.rb +1 -1
  55. data/lib/cantango/rails/helpers/rest_helper.rb +1 -1
  56. data/lib/cantango/rules/adaptor/active_record.rb +1 -4
  57. data/lib/cantango/rules/adaptor/data_mapper.rb +11 -0
  58. data/lib/cantango/rules/adaptor/mongo.rb +19 -0
  59. data/lib/cantango/rules/adaptor/mongo_mapper.rb +10 -0
  60. data/lib/cantango/rules/adaptor/mongoid.rb +1 -5
  61. data/lib/cantango/rules/adaptor/relational.rb +13 -0
  62. data/lib/cantango/rules/adaptor.rb +12 -7
  63. data/lib/cantango/rules/user_relation.rb +1 -2
  64. data/lib/cantango/user_ac_engine.rb +25 -7
  65. data/lib/cantango.rb +2 -0
  66. data/spec/cantango/ability/executor_spec.rb +67 -0
  67. data/spec/cantango/ability_executor/cached_only_spec.rb +1 -0
  68. data/spec/cantango/model/scope_spec.rb +11 -0
  69. data/spec/cantango/models/items.rb +5 -0
  70. data/spec/cantango/permission_engine_cached_spec.rb +51 -0
  71. data/spec/cantango/permission_engine_spec.rb +55 -0
  72. data/spec/cantango/permit_engine_cached_spec.rb +56 -0
  73. data/spec/cantango/permit_engine_spec.rb +57 -1
  74. data/spec/cantango/permits/executor_cached_spec.rb +0 -0
  75. data/spec/cantango/permits/executor_spec.rb +68 -0
  76. data/spec/cantango/user_ac_engine_cached_spec.rb +64 -0
  77. data/spec/cantango/user_ac_engine_spec.rb +14 -2
  78. data/spec/fixtures/models/items.rb +3 -0
  79. data/spec/fixtures/models/user.rb +18 -0
  80. metadata +55 -34
@@ -1,6 +1,8 @@
1
1
  module CanTango
2
2
  class Configuration
3
3
  class Models
4
+ autoload_modules :Generic, :ActiveRecord, :DataMapper, :MongoMapper, :Mongoid
5
+
4
6
  include Singleton
5
7
  include ClassExt
6
8
 
@@ -18,12 +20,35 @@ module CanTango
18
20
  end
19
21
  end
20
22
 
23
+ def exclude *names
24
+ @excluded = names.flatten.select_labels
25
+ end
26
+
27
+ def excluded
28
+ @excluded ||= []
29
+ end
30
+
21
31
  def available_models
22
- ar_models.map(&:name)
32
+ all_models - excluded.map {|m| m.to_s.camelize}
33
+ end
34
+
35
+ protected
36
+
37
+ def all_models
38
+ CanTango.config.orms.inject([]) do |result, orm|
39
+ result << adapter_for(orm).models.map(&:name)
40
+ result
41
+ end.flatten.compact
23
42
  end
24
43
 
25
44
  private
26
45
 
46
+
47
+
48
+ def adapter_for orm
49
+ "CanTango::Configuration::Models::#{orm.to_s.camlize}".constantize.new
50
+ end
51
+
27
52
  def try_model model_string
28
53
  model = try_class(model_string.singularize) || try_class(model_string)
29
54
  raise "No model #{model_string} defined!" if !model
@@ -33,7 +58,7 @@ module CanTango
33
58
  def grep reg_exp
34
59
  available_models.grep reg_exp
35
60
  end
36
-
61
+
37
62
  def ar_models
38
63
  # Sugar-high #to_strings didn't work here!
39
64
  ActiveRecord::Base.descendants
@@ -4,6 +4,7 @@ module CanTango
4
4
  include Singleton
5
5
 
6
6
  attr_reader :accounts
7
+ attr_writer :enabled_types
7
8
 
8
9
  def enabled_types
9
10
  @enabled_types || available_types
@@ -102,7 +103,7 @@ module CanTango
102
103
  end
103
104
 
104
105
  def key_for subject
105
- subject.kind_of?(CanTango::Ability) ? key_maker.create_for(subject) : key_maker.new(subject)
106
+ subject.respond_to?(:subject) ? key_maker.create_for(subject) : key_maker.new(subject)
106
107
  end
107
108
 
108
109
  def key_maker
@@ -35,6 +35,10 @@ module CanTango
35
35
  }
36
36
  end
37
37
 
38
+ def debug!
39
+ debug.set :on
40
+ end
41
+
38
42
  # Turn on all engines and enable compile adapter
39
43
  # i.e compilation of rules via sourcify
40
44
  def enable_defaults!
@@ -42,6 +46,15 @@ module CanTango
42
46
  adapters.use :compiler
43
47
  end
44
48
 
49
+ def enable_helpers *names
50
+ names = names.to_symbols
51
+ enable_rest_helper if names.include? :rest
52
+ end
53
+
54
+ def enable_rest_helper
55
+ ApplicationController.send :include, CanTango::Rails::Helpers::RestHelper
56
+ end
57
+
45
58
  def clear!
46
59
  CanTango::Configuration.components.each do |c|
47
60
  comp = send(c)
@@ -67,6 +80,7 @@ module CanTango
67
80
  engine
68
81
  end
69
82
 
83
+ attr_accessor :orms
70
84
  attr_writer :localhost_list
71
85
 
72
86
  def localhost_list
@@ -4,12 +4,14 @@ module CanTango
4
4
 
5
5
  attr_reader :ability
6
6
 
7
+ delegate :session, :user, :subject, :candidate, :cached?, :to => :ability
8
+
7
9
  def initialize ability
8
10
  @ability = ability
9
11
  end
10
12
 
11
13
  def execute!
12
- # raise NotImplementedError
14
+ raise NotImplementedError
13
15
  end
14
16
 
15
17
  def engine_name
@@ -23,31 +25,15 @@ module CanTango
23
25
  end
24
26
 
25
27
  def valid_cache_mode?
26
- modes.include?(:cache) && cache_mode?
28
+ modes.include?(:cache) && cached?
27
29
  end
28
30
 
29
31
  def valid_no_cache_mode?
30
- modes.include?(:no_cache) && !cache_mode?
32
+ modes.include?(:no_cache) && !cached?
31
33
  end
32
34
 
33
35
  def modes
34
36
  CanTango.config.engine(engine_name.to_sym).modes
35
37
  end
36
-
37
- def cache_mode?
38
- ability.cached?
39
- end
40
-
41
- def user
42
- ability.user
43
- end
44
-
45
- def subject
46
- ability.subject
47
- end
48
-
49
- def candidate
50
- ability.candidate
51
- end
52
38
  end
53
39
  end
@@ -14,10 +14,11 @@ module CanTango::Model
14
14
  include CanTango::Api::User::Ability
15
15
 
16
16
 
17
- attr_reader :actions, :clazz
17
+ attr_reader :actions, :mode, :clazz
18
18
 
19
- def initialize clazz, *actions
19
+ def initialize clazz, mode, *actions
20
20
  @clazz = clazz
21
+ @mode = mode
21
22
  @actions = actions.flatten
22
23
  end
23
24
 
@@ -33,17 +34,26 @@ module CanTango::Model
33
34
  protected
34
35
 
35
36
  def check ability
37
+ puts ability.rules.inspect
36
38
  clazz.all.select do |obj|
37
- actions.all? do |action|
38
- ability.can? action.to_sym, obj
39
+ actions.all? do |action|
40
+ ability.send mode_action, action.to_sym, obj
39
41
  end
40
42
  end
41
43
  end
44
+
45
+ def mode_action
46
+ "#{mode}?"
47
+ end
42
48
  end
43
49
 
44
50
  module ClassMethods
45
51
  def allowed_to *actions
46
- CanTango::Model::Scope::AllowedActions.new self, *actions
52
+ CanTango::Model::Scope::AllowedActions.new self, :can, *actions
53
+ end
54
+
55
+ def not_allowed_to *actions
56
+ CanTango::Model::Scope::AllowedActions.new self, :cannot, *actions
47
57
  end
48
58
 
49
59
  CanTango::Model::Scope.rest_actions.each do |action|
@@ -51,6 +61,10 @@ module CanTango::Model
51
61
  define_method :"#{meth_name}_by" do |user|
52
62
  all.select {|obj| obj.user_ability(user).can? action.to_sym, obj }
53
63
  end
64
+
65
+ define_method :"not_#{meth_name}_by" do |user|
66
+ all.select {|obj| obj.user_ability(user).cannot? action.to_sym, obj }
67
+ end
54
68
  end
55
69
  end
56
70
  end
@@ -1,9 +1,12 @@
1
1
  module CanTango
2
2
  class PermissionEngine < Engine
3
3
  class Collector
4
+ include CanTango::Helpers::Debug
5
+
4
6
  attr_reader :ability, :permissions, :type
5
7
 
6
8
  def initialize ability, permissions, type
9
+ debug "Collecting #{type} permissions"
7
10
  @ability = ability
8
11
  @permissions = permissions
9
12
  @type = type
@@ -1,6 +1,8 @@
1
1
  module CanTango
2
2
  class PermissionEngine < Engine
3
3
  class Evaluator
4
+ include CanTango::Helpers::Debug
5
+
4
6
  attr_reader :ability, :rule
5
7
 
6
8
  include CanTango::Rules
@@ -12,6 +14,9 @@ module CanTango
12
14
  end
13
15
 
14
16
  def evaluate! user
17
+ debug "Evaluating rule:"
18
+ debug rule.can
19
+ debug rule.cannot
15
20
  @user = user
16
21
  instance_eval rule.can if rule.can?
17
22
  instance_eval rule.cannot if rule.cannot?
@@ -1,6 +1,8 @@
1
1
  module CanTango
2
2
  class PermissionEngine < Engine
3
3
  class Factory
4
+ include CanTango::Helpers::Debug
5
+
4
6
  attr_accessor :ability
5
7
 
6
8
  # creates the factory for the ability
@@ -11,6 +13,7 @@ module CanTango
11
13
  end
12
14
 
13
15
  def build!
16
+ debug "building permissions"
14
17
  @evaluators ||= permission_types.inject([]) do |res, type|
15
18
  res << collector(type).build
16
19
  res
@@ -6,17 +6,16 @@ module CanTango
6
6
 
7
7
  def initialize file_name
8
8
  @file_name = file_name
9
-
10
9
  load!
11
10
  end
12
11
 
13
12
  def load_from_hash hash
14
13
  return if hash.empty?
15
14
  hash.each do |type, groups|
16
- permissions[type] ||= {}
17
-
18
- next if groups.nil?
19
-
15
+ permissions[type] ||= {}
16
+
17
+ next if groups.nil?
18
+
20
19
  groups.each do |group, rules|
21
20
  parser.parse(group, rules) do |permission|
22
21
  permissions[type][permission.name] = permission
@@ -46,13 +45,13 @@ module CanTango
46
45
 
47
46
  define_method(:"#{type}_compiled_permissions") do
48
47
  type_permissions = send(:"#{type}_permissions")
49
-
48
+
50
49
  return Hashie::Mash.new if !type_permissions || type_permissions.empty?
51
-
50
+
52
51
  compiled_sum = send(:"#{type}_permissions").inject({}) do |compiled_sum, (actor, permission)|
53
52
  compiled_sum.merge(permission.to_compiled_hash)
54
53
  end
55
-
54
+
56
55
  Hashie::Mash.new(compiled_sum)
57
56
  end
58
57
  end
@@ -21,7 +21,6 @@ module CanTango
21
21
  def save! permissions
22
22
  raise NotImplementedError
23
23
  end
24
-
25
24
  end
26
25
  end
27
26
  end
@@ -3,7 +3,7 @@ require 'yaml'
3
3
  module CanTango
4
4
  class PermissionEngine < Engine
5
5
  class YamlStore < Store
6
- attr_reader :path
6
+ attr_reader :path, :last_load_time
7
7
 
8
8
  # for a YamlStore, the name is the name of the yml file
9
9
  # options: extension, path
@@ -14,16 +14,27 @@ module CanTango
14
14
 
15
15
  def load!
16
16
  loader.load!
17
+ @last_load_time = Time.now
17
18
  end
18
19
 
19
20
  def load_from_hash hash
20
21
  loader.load_from_hash hash
21
22
  end
22
23
 
23
- # @stanislaw: don't like this, because what if loader#load! will be called
24
- # twice during object's (YamlStore.new) life time!
24
+ # return cached permissions if file has not changed since last load
25
+ # otherwise load permissions again to reflect changes!
25
26
  def permissions
26
- @permissions ||= loader.permissions
27
+ return @permissions if changed?
28
+ @permissions = loader.permissions
29
+ end
30
+
31
+ def changed?
32
+ return true if !last_load_time
33
+ last_modify_time <= last_load_time
34
+ end
35
+
36
+ def last_modify_time
37
+ File.mtime(file_path)
27
38
  end
28
39
 
29
40
  CanTango.config.permission_engine.types.each do |type|
@@ -4,13 +4,15 @@ module CanTango
4
4
  autoload_modules :Factory, :Loader, :Parser, :Permission
5
5
  autoload_modules :RulesParser, :Store, :YamlStore, :Statements, :Statement
6
6
 
7
+ include CanTango::Ability::Executor
8
+ include CanTango::Ability::RoleHelpers
9
+ include CanTango::Ability::UserHelpers
10
+
7
11
  def initialize ability
8
12
  super
9
13
  end
10
14
 
11
- def execute!
12
- return if !valid?
13
- debug "Permission Engine executing..."
15
+ def permit_rules
14
16
  permissions.each do |permission|
15
17
  permission.evaluate! user
16
18
  end
@@ -21,6 +23,7 @@ module CanTango
21
23
  end
22
24
 
23
25
  def valid?
26
+ puts "valid_mode? #{valid_mode?} #{modes} #{cached?}"
24
27
  return false if !valid_mode?
25
28
  permissions.empty? ? invalid : true
26
29
  end
@@ -31,13 +34,27 @@ module CanTango
31
34
 
32
35
  protected
33
36
 
37
+ alias_method :cache_key, :engine_name
38
+
39
+ def start_execute
40
+ debug "Permission Engine executing..."
41
+ end
42
+
43
+ def end_execute
44
+ debug "Done Permission Engine"
45
+ end
46
+
34
47
  def invalid
35
48
  debug "No permissions found!"
36
49
  false
37
50
  end
38
51
 
39
52
  def permission_factory
40
- @permission_factory ||= CanTango::PermissionEngine::Factory.new ability
53
+ @permission_factory ||= CanTango::PermissionEngine::Factory.new self
54
+ end
55
+
56
+ def changed?
57
+ permission_factory.store.changed?
41
58
  end
42
59
  end
43
60
  end
@@ -17,13 +17,19 @@ module CanTango
17
17
  permits
18
18
  end
19
19
 
20
+ # return hash of permits built, keyed by name of builder
20
21
  def permits
21
- @permits ||= builders.inject([]) do |permits, builder|
22
+ @permits ||= builders.inject({}) do |permits, builder|
22
23
  debug "++ Permit Builder: #{builder_class builder}"
23
24
  built_permits = permits_built_with(builder)
24
- debug "== Permits built: #{built_permits.size}"
25
- permits = permits + built_permits if built_permits
26
- end.flatten
25
+
26
+ if built_permits
27
+ debug "== Permits built: #{built_permits.size}"
28
+ permits[builder] = built_permits
29
+ end
30
+
31
+ permits
32
+ end
27
33
  end
28
34
 
29
35
  def permits_built_with builder
@@ -3,21 +3,26 @@ module CanTango
3
3
  autoload_modules :Builder, :Compatibility, :Executor
4
4
  autoload_modules :Factory, :Finder, :Loaders, :Util, :RoleMatcher
5
5
 
6
+ include CanTango::Ability::Executor
7
+ include CanTango::Ability::RoleHelpers
8
+ include CanTango::Ability::UserHelpers
9
+
6
10
  def initialize ability
7
11
  super
8
12
  end
9
13
 
10
- def execute!
11
- return if !valid?
12
- debug "Permit Engine executing..."
13
-
14
- # CanTango.config.permits.clear_executed! # should there be an option clear before each execution?
15
- permits.each do |permit|
16
- CanTango.config.permits.was_executed(permit, ability) if CanTango.config.debug.on?
17
- break if permit.execute == :break
14
+ def permit_rules
15
+ # push result of each permit type execution into main ability rules array
16
+ permits.each_pair do |type, permits|
17
+ perm_rules = executor(type, permits).execute!
18
+ rules << perm_rules if !perm_rules.blank?
18
19
  end
19
20
  end
20
21
 
22
+ def executor type, permits
23
+ CanTango::Permits::Executor.new self, type, permits
24
+ end
25
+
21
26
  def engine_name
22
27
  :permit
23
28
  end
@@ -40,13 +45,38 @@ module CanTango
40
45
 
41
46
  protected
42
47
 
48
+ alias_method :cache_key, :engine_name
49
+
50
+ def start_execute
51
+ debug "Permit Engine executing..."
52
+ end
53
+
54
+ def end_execute
55
+ debug "Done Permit Engine"
56
+ end
57
+
43
58
  def invalid
44
59
  debug "No permits found!"
45
60
  false
46
61
  end
47
62
 
48
63
  def permit_factory
49
- @permit_factory ||= CanTango::PermitEngine::Factory.new ability
64
+ @permit_factory ||= CanTango::PermitEngine::Factory.new self
65
+ end
66
+
67
+ def key_method_names
68
+ permits.keys.map {|type| key type }.compact
69
+ end
70
+
71
+ def key type
72
+ case type
73
+ when :role
74
+ roles_list_meth
75
+ when :role_group
76
+ role_groups_list_meth
77
+ else
78
+ nil
79
+ end
50
80
  end
51
81
  end
52
82
  end
@@ -8,15 +8,19 @@ module CanTango
8
8
  # @return [Array<RoleGroupPermit::Base>] the role permits built for this ability
9
9
  def build
10
10
  return [] if !user_account
11
- puts debug_msg if CanTango.debug?
11
+ puts debug_msg if CanTango.debug?
12
12
  [permit].compact
13
13
  end
14
14
 
15
+ def name
16
+ :account
17
+ end
18
+
15
19
  protected
16
20
 
17
21
  def debug_msg
18
22
  permit ? "Building AccountPermit for #{user_account}, permit: #{permit}" : "Not building any AccountPermit"
19
- end
23
+ end
20
24
 
21
25
  def permit
22
26
  create_permit(user_account.class.to_s)
@@ -2,44 +2,24 @@
2
2
  # which can be cached under some key and later reused
3
3
  #
4
4
  module CanTango
5
- class UserAcEngine < Engine
5
+ module Permits
6
6
  class Executor
7
- include CanTango::Ability::CacheHelpers
7
+ include CanTango::Ability::Executor
8
8
 
9
- attr_reader :ability, :permits
9
+ attr_reader :ability, :permit_type, :permits
10
10
 
11
- delegate :session, :user, :subject, :cached?, :to => :ability
12
-
13
- def initialize ability, permit_type, permissions
14
- @ability = ability
15
- @permissions = permissions
11
+ def initialize ability, permit_type, permits
12
+ @ability = ability
13
+ @permit_type = permit_type
14
+ @permits = permits
16
15
  end
17
16
 
18
- def cache_key
19
- :user_ac
20
- end
21
-
22
- def rules
23
- @rules ||= []
24
- end
25
-
26
- def clear_rules!
27
- @rules ||= []
28
- end
17
+ alias_method :cache_key, :permit_type
29
18
 
30
19
  def cache
31
20
  @cache ||= CanTango::Ability::Cache.new self, :cache_key => cache_key, :key_method_names => key_method_names
32
21
  end
33
22
 
34
- def execute!
35
- return if cached_rules?
36
-
37
- clear_rules!
38
- permit_rules
39
-
40
- cache_rules!
41
- end
42
-
43
23
  def permit_rules
44
24
  # TODO: somehow type specific caching of result of permits!
45
25
  permits.each do |permit|
@@ -50,10 +30,28 @@ module CanTango
50
30
 
51
31
  protected
52
32
 
33
+ def valid?
34
+ true
35
+ end
36
+
37
+ def start_execute
38
+ debug "Execute #{permit_type} permits"
39
+ end
40
+
41
+ def end_execute
42
+ debug "Done #{permit_type} permits"
43
+ end
44
+
53
45
  def key_method_names
54
- [:permissions_hash]
46
+ case permit_type
47
+ when :role
48
+ [roles_list_meth]
49
+ when :role_group
50
+ [role_groups_list_meth]
51
+ else
52
+ []
53
+ end
55
54
  end
56
55
  end
57
56
  end
58
57
  end
59
-
@@ -0,0 +1,21 @@
1
+ module CanTango
2
+ module Permits
3
+ class Permit
4
+ module ClassMethods
5
+ def first_name clazz
6
+ clazz.to_s.gsub(/^([A-Za-z]+).*/, '\1').underscore.to_sym # first part of class name
7
+ end
8
+
9
+ def type
10
+ :abstract
11
+ end
12
+
13
+ def account_name clazz
14
+ return nil if clazz.name == clazz.name.demodulize
15
+ clazz.name.gsub(/::.*/,'').gsub(/(.*)Permits/, '\1').underscore.to_sym
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+