active_element 0.0.2 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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