active_element 0.0.2 → 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +1 -0
  3. data/Gemfile +1 -1
  4. data/Gemfile.lock +8 -9
  5. data/active_element.gemspec +1 -1
  6. data/app/assets/javascripts/active_element/application.js +3 -1
  7. data/app/assets/javascripts/active_element/popover.js +6 -0
  8. data/app/assets/javascripts/active_element/setup.js +12 -0
  9. data/app/assets/javascripts/active_element/{search_field.js → text_search_field.js} +2 -2
  10. data/app/assets/javascripts/active_element/toast.js +8 -0
  11. data/app/assets/stylesheets/active_element/application.scss +65 -1
  12. data/app/controllers/active_element/application_controller.rb +13 -19
  13. data/app/views/active_element/components/form/_label.html.erb +2 -2
  14. data/app/views/active_element/components/form/_templates.html.erb +8 -8
  15. data/app/views/active_element/components/form.html.erb +3 -3
  16. data/app/views/active_element/components/table/_collection_row.html.erb +3 -3
  17. data/app/views/active_element/components/table/collection.html.erb +1 -1
  18. data/app/views/active_element/components/table/item.html.erb +3 -3
  19. data/app/views/active_element/navbar/_menu.html.erb +2 -2
  20. data/app/views/layouts/active_element.html.erb +21 -23
  21. data/app/views/layouts/active_element_error.html.erb +1 -1
  22. data/config/routes.rb +10 -1
  23. data/lib/active_element/components/button.rb +6 -41
  24. data/lib/active_element/components/form.rb +12 -3
  25. data/lib/active_element/components/text_search/active_record_authorization.rb +13 -0
  26. data/lib/active_element/components/text_search/authorization.rb +117 -0
  27. data/lib/active_element/components/text_search/component.rb +118 -0
  28. data/lib/active_element/components/text_search/sql.rb +107 -0
  29. data/lib/active_element/components/text_search.rb +23 -0
  30. data/lib/active_element/components/util/decorator.rb +2 -2
  31. data/lib/active_element/components/util/record_path.rb +84 -0
  32. data/lib/active_element/components/util.rb +7 -2
  33. data/lib/active_element/components.rb +1 -0
  34. data/lib/active_element/controller_action.rb +9 -10
  35. data/lib/active_element/controller_interface.rb +78 -0
  36. data/lib/active_element/permissions_check.rb +33 -29
  37. data/lib/active_element/permissions_report.rb +57 -0
  38. data/lib/active_element/route.rb +1 -1
  39. data/lib/active_element/routes.rb +1 -1
  40. data/lib/active_element/version.rb +1 -1
  41. data/lib/active_element.rb +24 -10
  42. data/lib/tasks/active_element.rake +1 -16
  43. data/rspec-documentation/pages/Components/Tables.md +2 -2
  44. data/rspec-documentation/spec_helper.rb +1 -1
  45. metadata +19 -12
  46. data/app/controllers/active_element/text_searches_controller.rb +0 -189
  47. data/lib/active_element/active_record_text_search_authorization.rb +0 -12
  48. data/lib/active_element/colorized_string.rb +0 -33
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveElement
4
+ # Provides the `active_element` object available on all controller instance/class methods.
5
+ # Encapsulates core functionality such as `authenticate_with`, `permit_action`, and `component`
6
+ # without polluting application controller namespace.
7
+ class ControllerInterface
8
+ attr_reader :missing_template_store, :current_user
9
+
10
+ @state = {}
11
+
12
+ class << self
13
+ attr_reader :state
14
+ end
15
+
16
+ def initialize(controller_class, controller_instance = nil)
17
+ @controller_class = controller_class
18
+ @controller_instance = controller_instance
19
+ initialize_state
20
+ @missing_template_store = {}
21
+ @authorize = false
22
+ end
23
+
24
+ def authorize?
25
+ @authorize
26
+ end
27
+
28
+ def application_name
29
+ RailsComponent.new(::Rails).application_name
30
+ end
31
+
32
+ def authenticate_with(&block)
33
+ state[:authenticator] = block
34
+ end
35
+
36
+ def authorize_with(&block)
37
+ @authorize = true
38
+ @current_user = block.call
39
+ end
40
+
41
+ def authenticate
42
+ authenticator&.call
43
+ end
44
+
45
+ def permit_action(action, with: nil, always: false)
46
+ raise ArgumentError, "Must specify `with: '<permission>'` or `always: true`" unless with.present? || always
47
+ raise ArgumentError, 'Cannot specify both `with` and `always: true`' if with.present? && always
48
+
49
+ state[:permissions] << { with: with, always: always, action: action }
50
+ end
51
+
52
+ def authenticator
53
+ state[:authenticator]
54
+ end
55
+
56
+ def permissions
57
+ state.fetch(:permissions)
58
+ end
59
+
60
+ def component
61
+ return (@component ||= ActiveElement::Component.new(controller_instance)) unless controller_instance.nil?
62
+
63
+ raise ArgumentError, 'Attempted to use ActiveElement component from a controller class method.'
64
+ end
65
+
66
+ private
67
+
68
+ attr_reader :controller_class, :controller_instance
69
+
70
+ def initialize_state
71
+ self.class.state[controller_class] ||= { permissions: [], authenticator: nil }
72
+ end
73
+
74
+ def state
75
+ self.class.state[controller_class]
76
+ end
77
+ end
78
+ end
@@ -3,6 +3,8 @@
3
3
  module ActiveElement
4
4
  # Verifies provided permissions against required permissions.
5
5
  class PermissionsCheck
6
+ include Paintbrush
7
+
6
8
  def initialize(required:, actual:, controller_path:, action_name:, rails_component:)
7
9
  @required = required.presence || []
8
10
  @actual = normalized(actual)
@@ -18,41 +20,58 @@ module ActiveElement
18
20
 
19
21
  def message
20
22
  return development_environment_message if rails_component.environment == 'development'
21
- return "User access granted for permission(s): #{applicable.join(', ')}" if permitted?
23
+ return permitted_message if permitted?
22
24
 
23
- "User access forbidden. Missing user permission(s): #{missing.join(', ')}"
25
+ forbidden_message
24
26
  end
25
27
 
26
28
  def missing
27
29
  @missing ||= applicable.reject do |permission|
28
- actual.include?(permission.to_s)
29
- end.map(&:to_s)
30
+ next true if permission.fetch(:always, false)
31
+
32
+ actual.include?(permission.fetch(:with).to_s)
33
+ end
30
34
  end
31
35
 
32
36
  def applicable
33
37
  @applicable ||= default_permissions + required_permissions
34
38
  end
35
39
 
40
+ def applicable_permissions
41
+ applicable.map { |permission| permission.fetch(:with) }
42
+ end
43
+
44
+ def missing_permissions
45
+ missing.map { |permission| permission.fetch(:with) }
46
+ end
47
+
36
48
  private
37
49
 
38
50
  attr_reader :required, :actual, :controller_name, :action_name, :rails_component
39
51
 
40
52
  def development_environment_message
41
- "Bypassed permission(s) in development environment: #{applicable.join(', ')}"
53
+ "Bypassed permission(s) in development environment: #{applicable_permissions.join(', ')}"
54
+ end
55
+
56
+ def permitted_message
57
+ paintbrush { green "User access granted for permission(s): #{cyan applicable_permissions.join(', ')}" }
58
+ end
59
+
60
+ def forbidden_message
61
+ paintbrush { red "User access forbidden. Missing user permission(s): #{cyan missing_permissions.join(', ')}" }
42
62
  end
43
63
 
44
64
  def default_permissions
45
65
  return [] if normalized_action.nil?
46
66
 
47
- ["can_#{normalized_action}_#{rails_component.application_name}_#{controller_name}"]
67
+ [{
68
+ action: normalized_action,
69
+ with: "can_#{normalized_action}_#{rails_component.application_name}_#{controller_name}"
70
+ }]
48
71
  end
49
72
 
50
73
  def required_permissions
51
- @required_permissions ||= required.map do |permission, options|
52
- next nil unless applicable?(options)
53
-
54
- permission
55
- end.compact
74
+ @required_permissions ||= required.select { |options| applicable?(options) }
56
75
  end
57
76
 
58
77
  def normalized_action
@@ -74,28 +93,13 @@ module ActiveElement
74
93
  end
75
94
 
76
95
  def applicable?(options)
77
- return true if !options.key?(:only) && !options.key?(:except)
78
- return true if only_applicable?(options)
79
- return false if except_applicable?(options)
80
-
81
- false
82
- end
83
-
84
- def only_applicable?(options)
85
- return false unless options.key?(:only)
86
-
87
- normalized(options.fetch(:only)).include?(action_name)
88
- end
89
-
90
- def except_applicable?(options)
91
- return false unless options.key?(:except)
92
-
93
- normalized(options.fetch(:except)).include?(action_name)
96
+ options.fetch(:action).to_s == action_name
94
97
  end
95
98
 
96
99
  def raise_unprotected_route_error
97
100
  raise UnprotectedRouteError,
98
- "#{controller_name.titleize.tr(' ', '')}##{action_name} must be protected with `permit_user`"
101
+ "#{controller_name.titleize.tr(' ', '')}##{action_name} must be protected with " \
102
+ '`active_element.permit_action`'
99
103
  end
100
104
  end
101
105
  end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveElement
4
+ # Generates a report of all permissions used by a given application. Used by `rake active_element:permissions`
5
+ # to provide a convenient interface to listing all required permissions so that they can be provisioned to
6
+ # users as needed.
7
+ class PermissionsReport
8
+ include Paintbrush
9
+
10
+ COLOR_MAP = { list: :cyan, view: :blue, create: :green, delete: :red, edit: :yellow, text: :white }.freeze
11
+
12
+ def initialize
13
+ ActiveElement.eager_load_controllers
14
+ ActiveElement.eager_load_models
15
+ @buffer = []
16
+ end
17
+
18
+ def report
19
+ generate
20
+ buffer.join("\n")
21
+ end
22
+
23
+ private
24
+
25
+ attr_reader :buffer
26
+
27
+ def generate
28
+ buffer << paintbrush { blue "\nThe following user permissions are used by this application:\n" }
29
+ permissions.each do |permission|
30
+ buffer << paintbrush { white " * #{public_send(color(permission), permission)}" }
31
+ end
32
+ buffer << "\n"
33
+ end
34
+
35
+ def color(permission)
36
+ COLOR_MAP.find do |action, _|
37
+ permission.include?("_#{action}_")
38
+ end&.last || :purple
39
+ end
40
+
41
+ def permissions
42
+ routes.map(&:permissions).flatten.sort.uniq + text_search_permissions
43
+ end
44
+
45
+ def text_search_permissions
46
+ ActiveElement::Components::TextSearch.authorized_text_searches.map do |model, with, providing|
47
+ (Array(with) + Array(providing)).map do |field|
48
+ ActiveElement::Components::TextSearch::Authorization.permission_for(model: model, field: field)
49
+ end
50
+ end.flatten.uniq
51
+ end
52
+
53
+ def routes
54
+ ActiveElement::Routes.new(rails_component: ActiveElement::RailsComponent.new(Rails))
55
+ end
56
+ end
57
+ end
@@ -49,7 +49,7 @@ module ActiveElement
49
49
  end
50
50
 
51
51
  def permissions
52
- permissions_check.applicable.map(&:to_s)
52
+ permissions_check.applicable.map { |permission| permission.fetch(:with).to_s }
53
53
  end
54
54
 
55
55
  def rails_route?
@@ -41,7 +41,7 @@ module ActiveElement
41
41
 
42
42
  def descendants_with_permissions
43
43
  @descendants_with_permissions ||= descendants.map do |controller_class|
44
- [controller_class.new, controller_class.active_element_permissions]
44
+ [controller_class.new, controller_class.active_element.permissions]
45
45
  end.compact
46
46
  end
47
47
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveElement
4
- VERSION = '0.0.2'
4
+ VERSION = '0.0.4'
5
5
  end
@@ -1,18 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'faraday'
4
3
  require 'rouge'
5
4
  require 'kaminari'
6
5
  require 'sassc'
7
6
  require 'bootstrap'
8
7
  require 'active_record'
9
- # require 'rspec/documentation' # FIXME: Load dynamically when running rspec documentation command.
8
+ require 'paintbrush'
10
9
 
11
10
  require_relative 'active_element/version'
12
- require_relative 'active_element/active_record_text_search_authorization'
13
- require_relative 'active_element/colorized_string'
14
11
  require_relative 'active_element/active_menu_link'
15
12
  require_relative 'active_element/permissions_check'
13
+ require_relative 'active_element/permissions_report'
14
+ require_relative 'active_element/controller_interface'
16
15
  require_relative 'active_element/controller_action'
17
16
  require_relative 'active_element/rails_component'
18
17
  require_relative 'active_element/route'
@@ -30,6 +29,8 @@ module ActiveElement
30
29
  class << self
31
30
  attr_writer :application_name, :navbar_items
32
31
 
32
+ include Paintbrush
33
+
33
34
  def application_title
34
35
  @application_name || RailsComponent.new(Rails).application_name.titleize
35
36
  end
@@ -38,6 +39,14 @@ module ActiveElement
38
39
  @navbar_items || inferred_navbar_items(user)
39
40
  end
40
41
 
42
+ def warning(message)
43
+ warn "#{log_tag} #{paintbrush { yellow(message) }}"
44
+ end
45
+
46
+ def log_tag
47
+ paintbrush { cyan "[#{blue 'ActiveElement'}]" }
48
+ end
49
+
41
50
  def active_path_class(user:, current_navbar_item:, current_path:, controller_path:, action_name:)
42
51
  if ActiveMenuLink.new(
43
52
  rails_component: RailsComponent.new(Rails),
@@ -71,12 +80,17 @@ module ActiveElement
71
80
  false
72
81
  end
73
82
 
83
+ def eager_load_models
84
+ eager_load(:models)
85
+ end
86
+
74
87
  def eager_load_controllers
75
- Pathname.new(__dir__)
76
- .join('../app/controllers/active_element')
77
- .glob('**/*_controller.rb')
78
- .each { |path| require path }
79
- Rails.root.join('app/controllers/admin/').glob('**/*_controller.rb').each { |path| require path }
88
+ eager_load(:controllers)
89
+ end
90
+
91
+ def eager_load(resource)
92
+ suffix = resource == :controllers ? '_controller' : nil
93
+ Rails.root.join("app/#{resource}").glob("**/*#{suffix}.rb").each { |path| require path }
80
94
  end
81
95
 
82
96
  private
@@ -90,7 +104,7 @@ module ActiveElement
90
104
 
91
105
  def user_routes(user)
92
106
  ActiveElement::Routes.new(
93
- permissions: user.permissions,
107
+ permissions: user&.permissions,
94
108
  rails_component: ActiveElement::RailsComponent.new(Rails)
95
109
  )
96
110
  end
@@ -3,21 +3,6 @@
3
3
  namespace :active_element do
4
4
  desc 'Displays all permissions used by this application'
5
5
  task permissions: :environment do
6
- ActiveElement.eager_load_controllers
7
- routes = ActiveElement::Routes.new(rails_component: ActiveElement::RailsComponent.new(Rails))
8
- permissions = routes.map(&:permissions).flatten.sort.uniq
9
- $stdout.puts ActiveElement::ColorizedString.new(
10
- "\nThe following user permissions are used by this application:\n",
11
- color: :light_blue
12
- ).value
13
- permissions.each do |permission|
14
- color = { list: :cyan, view: :blue, create: :green, delete: :red, edit: :yellow }.find do |action, _|
15
- permission.include?("_#{action}_")
16
- end&.last || :purple
17
- $stdout.puts(' ' \
18
- "#{ActiveElement::ColorizedString.new('*', color: :white).value} " \
19
- "#{ActiveElement::ColorizedString.new(permission, color: color).value}")
20
- end
21
- $stdout.puts
6
+ $stdout.puts ActiveElement::PermissionsReport.new.report
22
7
  end
23
8
  end
@@ -15,7 +15,7 @@ collection = [
15
15
  User.new(name: 'Jane', email: 'jane@example.org')
16
16
  ]
17
17
 
18
- html = active_element_component.table collection: collection, fields: [:name, :email]
18
+ html = active_element.component.table collection: collection, fields: [:name, :email]
19
19
 
20
20
  it_documents html do
21
21
  expect(html).to include 'John'
@@ -32,7 +32,7 @@ end
32
32
 
33
33
  item = User.new(name: 'John', email: 'john@example.com', overview: 'Writes Ruby code for a living.')
34
34
 
35
- html = active_element_component.table item: item, fields: [:name, :email, :password, :secret]
35
+ html = active_element.component.table item: item, fields: [:name, :email, :password, :secret]
36
36
 
37
37
  it_documents html do
38
38
  expect(html).to include 'John'
@@ -30,6 +30,6 @@ RSpec::Documentation.configure do |config|
30
30
  end
31
31
  end
32
32
 
33
- context.active_element_component = ActiveElement::Component.new(StubbedController.new)
33
+ context.active_element.component = ActiveElement::Component.new(StubbedController.new)
34
34
  end
35
35
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: active_element
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.0.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Bob Farrell
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-05-23 00:00:00.000000000 Z
11
+ date: 2023-05-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bootstrap
@@ -25,33 +25,33 @@ dependencies:
25
25
  - !ruby/object:Gem::Version
26
26
  version: 5.3.0alpha3
27
27
  - !ruby/object:Gem::Dependency
28
- name: faraday
28
+ name: kaminari
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: '2.7'
33
+ version: '1.2'
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
- version: '2.7'
40
+ version: '1.2'
41
41
  - !ruby/object:Gem::Dependency
42
- name: kaminari
42
+ name: paintbrush
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
45
  - - "~>"
46
46
  - !ruby/object:Gem::Version
47
- version: '1.2'
47
+ version: 0.1.1
48
48
  type: :runtime
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
- version: '1.2'
54
+ version: 0.1.1
55
55
  - !ruby/object:Gem::Dependency
56
56
  name: rails
57
57
  requirement: !ruby/object:Gem::Requirement
@@ -118,14 +118,15 @@ files:
118
118
  - app/assets/javascripts/active_element/form.js
119
119
  - app/assets/javascripts/active_element/json_field.js
120
120
  - app/assets/javascripts/active_element/pagination.js
121
- - app/assets/javascripts/active_element/search_field.js
121
+ - app/assets/javascripts/active_element/popover.js
122
122
  - app/assets/javascripts/active_element/secret.js
123
123
  - app/assets/javascripts/active_element/setup.js
124
+ - app/assets/javascripts/active_element/text_search_field.js
124
125
  - app/assets/javascripts/active_element/theme.js
126
+ - app/assets/javascripts/active_element/toast.js
125
127
  - app/assets/stylesheets/active_element/_variables.scss
126
128
  - app/assets/stylesheets/active_element/application.scss
127
129
  - app/controllers/active_element/application_controller.rb
128
- - app/controllers/active_element/text_searches_controller.rb
129
130
  - app/views/active_element/components/_horizontal_tabs.html.erb
130
131
  - app/views/active_element/components/_vertical_tabs.html.erb
131
132
  - app/views/active_element/components/button.html.erb
@@ -175,8 +176,6 @@ files:
175
176
  - config/routes.rb
176
177
  - lib/active_element.rb
177
178
  - lib/active_element/active_menu_link.rb
178
- - lib/active_element/active_record_text_search_authorization.rb
179
- - lib/active_element/colorized_string.rb
180
179
  - lib/active_element/component.rb
181
180
  - lib/active_element/components.rb
182
181
  - lib/active_element/components/button.rb
@@ -189,6 +188,11 @@ files:
189
188
  - lib/active_element/components/secret_fields.rb
190
189
  - lib/active_element/components/tab.rb
191
190
  - lib/active_element/components/tabs.rb
191
+ - lib/active_element/components/text_search.rb
192
+ - lib/active_element/components/text_search/active_record_authorization.rb
193
+ - lib/active_element/components/text_search/authorization.rb
194
+ - lib/active_element/components/text_search/component.rb
195
+ - lib/active_element/components/text_search/sql.rb
192
196
  - lib/active_element/components/translations.rb
193
197
  - lib/active_element/components/util.rb
194
198
  - lib/active_element/components/util/association_mapping.rb
@@ -199,9 +203,12 @@ files:
199
203
  - lib/active_element/components/util/form_value_mapping.rb
200
204
  - lib/active_element/components/util/i18n.rb
201
205
  - lib/active_element/components/util/record_mapping.rb
206
+ - lib/active_element/components/util/record_path.rb
202
207
  - lib/active_element/controller_action.rb
208
+ - lib/active_element/controller_interface.rb
203
209
  - lib/active_element/engine.rb
204
210
  - lib/active_element/permissions_check.rb
211
+ - lib/active_element/permissions_report.rb
205
212
  - lib/active_element/rails_component.rb
206
213
  - lib/active_element/route.rb
207
214
  - lib/active_element/routes.rb