rhino-rails 4.3.0 → 4.4.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 43ae824cbc25eeacd6ce09fc6a9d5b218206d8877fc672a49b728dd6fbf10b1c
4
- data.tar.gz: 6590704092005ad3b6c0b959b0166332387b2503d8db19e57fc17be7734c2d0b
3
+ metadata.gz: 0155b30eedf93061b38295ed96254194529de0abf03aad309becc312a512c5ba
4
+ data.tar.gz: 5e1ce1624bab2a77f8603ebdf6984a43e4bd71d277f51900ae533465e4fd0881
5
5
  SHA512:
6
- metadata.gz: a8007c1c7f90ceae1cb0aed88ca032009e417f0e8052897f5ed73e514d9f3c782c6304f92f9b16002179d6492f5510d7ef3a3e00f5902094a925c2dd74c3b781
7
- data.tar.gz: a58f92293f66ad18700a9ec39bf48e0696f64c72b7ac82c64fde715775821a5f00c3b90ce8e61dbf76b4edf4973eb520bc04b075e04ffb16c84fe7253c350e74
6
+ metadata.gz: 975f1ee3efc83423ae775131eace5689a7b09269541848da21e286f2f55cedd71b265a8e3c5c448a85cc7b9c8ecdde2e27dd615ef1a26719732a7d958cbfb8da
7
+ data.tar.gz: 3a32ccaf04ad8f351828752a83efac31503d53afbd4c3239d0f38e836c8e151dd4681f2a650a853e9d6d728e6477f34e72023680c6bbd36dc63369ad6ca6d10c
data/README.md CHANGED
@@ -40,6 +40,7 @@ Register a model, get a full REST API instantly.
40
40
  | 26 | **Generator CLI** | `rhino:install`, `rhino:generate`, `rhino:blueprint`, `rhino:export_postman`. |
41
41
  | 27 | **Postman Export** | Auto-generated Postman Collection v2.1 with all endpoints. |
42
42
  | 28 | **Blueprint System** | YAML-to-code generation for models, migrations, factories, policies, tests, and seeders. |
43
+ | 29 | **Named Scopes** | `?scope=availableForDrivers` client-selectable scopes (whitelisted via `rhino_scopes`), plus a `rhino_default_scope` applied when none is requested. Unknown scopes return 403. Applies to `index`/`trashed` only. |
43
44
 
44
45
  ## Quick Start
45
46
 
@@ -13,6 +13,8 @@ module Rhino
13
13
  # rhino_includes :user, :comments
14
14
  # rhino_fields :id, :title, :status, :created_at
15
15
  # rhino_search :title, :content, 'user.name'
16
+ # rhino_scopes :active, available_for_drivers: Scopes::AvailableForDriversScope
17
+ # rhino_default_scope :active
16
18
  # rhino_per_page 25
17
19
  # rhino_pagination_enabled true
18
20
  # rhino_middleware 'throttle:60,1'
@@ -24,6 +26,8 @@ module Rhino
24
26
 
25
27
  included do
26
28
  class_attribute :allowed_filters, default: []
29
+ class_attribute :allowed_scopes, default: {}
30
+ class_attribute :default_rhino_scope, default: nil
27
31
  class_attribute :allowed_sorts, default: []
28
32
  class_attribute :default_sort_field, default: nil
29
33
  class_attribute :allowed_includes, default: []
@@ -42,6 +46,23 @@ module Rhino
42
46
  self.allowed_filters = fields.map(&:to_s)
43
47
  end
44
48
 
49
+ # Whitelist client-selectable named scopes for ?scope=.
50
+ # rhino_scopes :active, available_for_drivers: Scopes::AvailableForDriversScope
51
+ # Bare symbols must name an existing ActiveRecord scope/class method on the model.
52
+ # Hash values may be a Proc(relation, user) or a Rhino::ResourceScope subclass.
53
+ def rhino_scopes(*names, **named)
54
+ merged = allowed_scopes.dup
55
+ names.each { |n| merged[n.to_s] = n.to_sym }
56
+ named.each { |k, v| merged[k.to_s] = v }
57
+ self.allowed_scopes = merged
58
+ end
59
+
60
+ # Named scope applied when no ?scope param is sent. Convenience, not a
61
+ # security boundary. Value is the scope name (string/symbol).
62
+ def rhino_default_scope(name)
63
+ self.default_rhino_scope = name.to_s
64
+ end
65
+
45
66
  def rhino_sorts(*fields)
46
67
  self.allowed_sorts = fields.map(&:to_s)
47
68
  end
@@ -16,6 +16,10 @@ module Rhino
16
16
  render json: { message: "This action is unauthorized." }, status: :forbidden
17
17
  end
18
18
 
19
+ rescue_from Rhino::ScopeNotAllowedError do |e|
20
+ render json: { message: "Scope '#{e.message}' is not allowed" }, status: :forbidden
21
+ end
22
+
19
23
  # Cache for auto-detected organization paths (class-level, survives across requests)
20
24
  @@organization_path_cache = {}
21
25
 
@@ -38,7 +42,7 @@ module Rhino
38
42
  def index
39
43
  authorize model_class, :index?, policy_class: policy_for(model_class)
40
44
 
41
- builder = QueryBuilder.new(model_class, params: params)
45
+ builder = QueryBuilder.new(model_class, params: params, named_scopes: true)
42
46
  apply_organization_scope(builder)
43
47
  builder.build
44
48
 
@@ -170,7 +174,7 @@ module Rhino
170
174
  def trashed
171
175
  authorize model_class, :view_trashed?, policy_class: policy_for(model_class)
172
176
 
173
- builder = QueryBuilder.new(model_class.discarded, params: params)
177
+ builder = QueryBuilder.new(model_class.discarded, params: params, named_scopes: true)
174
178
  apply_organization_scope(builder)
175
179
  builder.build
176
180
 
@@ -900,7 +904,7 @@ module Rhino
900
904
  end
901
905
 
902
906
  def params_hash
903
- params.except(:controller, :action, :model_slug, :route_group, :organization, :id, :format).to_unsafe_h
907
+ params.except(:controller, :action, :model_slug, :route_group, :organization, :id, :format, :scope).to_unsafe_h
904
908
  end
905
909
  end
906
910
  end
@@ -104,6 +104,36 @@ module Rhino
104
104
  # self.allowed_filters = %w[status user_id category_id]
105
105
  self.allowed_filters = []
106
106
 
107
+ # @!attribute [rw] allowed_scopes
108
+ # Client-selectable named scopes (whitelist for +?scope=+).
109
+ #
110
+ # Controls which named scopes can be requested via +?scope=name+
111
+ # (camelCase on the wire, underscored internally). Only whitelisted
112
+ # scopes are accepted — unknown/unlisted names return 403.
113
+ #
114
+ # Set via DSL: +rhino_scopes :active, available_for_drivers: Scopes::AvailableForDriversScope+
115
+ #
116
+ # Query: +GET /api/posts?scope=availableForDrivers+
117
+ #
118
+ # @return [Hash{String => Symbol, Proc, Class}]
119
+ # @example
120
+ # rhino_scopes :active, available_for_drivers: Scopes::AvailableForDriversScope
121
+ self.allowed_scopes = {}
122
+
123
+ # @!attribute [rw] default_rhino_scope
124
+ # Named scope applied automatically when no +?scope+ param is sent.
125
+ #
126
+ # This is a listing convenience, not a security boundary. The default
127
+ # scope is always requestable by name even if it is not otherwise
128
+ # whitelisted via +rhino_scopes+.
129
+ #
130
+ # Set via DSL: +rhino_default_scope :active+
131
+ #
132
+ # @return [String, nil]
133
+ # @example
134
+ # rhino_default_scope :active
135
+ self.default_rhino_scope = nil
136
+
107
137
  # @!attribute [rw] allowed_sorts
108
138
  # Sortable columns.
109
139
  #
@@ -14,14 +14,16 @@ module Rhino
14
14
  class QueryBuilder
15
15
  attr_reader :scope, :model_class, :params
16
16
 
17
- def initialize(model_class, params: {})
17
+ def initialize(model_class, params: {}, named_scopes: false)
18
18
  @model_class = model_class
19
19
  @scope = model_class.all
20
20
  @params = params
21
+ @named_scopes = named_scopes
21
22
  end
22
23
 
23
24
  # Apply all query modifications based on params and model config.
24
25
  def build
26
+ apply_named_scope if @named_scopes
25
27
  apply_filters
26
28
  apply_default_sort
27
29
  apply_sorts
@@ -62,6 +64,41 @@ module Rhino
62
64
 
63
65
  private
64
66
 
67
+ # ------------------------------------------------------------------
68
+ # Named scopes: ?scope=availableForDrivers
69
+ # ------------------------------------------------------------------
70
+ #
71
+ # Only runs for collection endpoints (index/trashed), which pass
72
+ # +named_scopes: true+. `show` (including its ?include= build path) stays
73
+ # unscoped so a record excluded by the default scope is still viewable.
74
+ def apply_named_scope
75
+ requested = params[:scope].presence
76
+ name = requested ? requested.to_s.underscore : model_class.try(:default_rhino_scope)
77
+ return unless name
78
+
79
+ allowed = model_class.try(:allowed_scopes) || {}
80
+ entry = allowed[name]
81
+ # The default scope is implicitly allowed when requested by name.
82
+ entry ||= name.to_sym if name == model_class.try(:default_rhino_scope)
83
+
84
+ # Echo the client's wire name (not the underscored form) in the error.
85
+ raise Rhino::ScopeNotAllowedError, (requested ? requested.to_s : name) if entry.nil?
86
+
87
+ user = defined?(RequestStore) ? RequestStore.store[:rhino_current_user] : nil
88
+
89
+ @scope =
90
+ case entry
91
+ when Symbol
92
+ # Whitelisted AR scope. Client input never reaches public_send unless the
93
+ # developer declared it via rhino_scopes. .merge composes with default_scopes.
94
+ @scope.merge(model_class.public_send(entry))
95
+ when Proc
96
+ entry.call(@scope, user)
97
+ else
98
+ entry.new.apply(@scope) # Rhino::ResourceScope subclass (user/org/role helpers)
99
+ end
100
+ end
101
+
65
102
  # ------------------------------------------------------------------
66
103
  # Filtering: ?filter[status]=published&filter[user_id]=1
67
104
  # ------------------------------------------------------------------
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rhino
4
+ # Raised when a client requests a ?scope= name that is not whitelisted via
5
+ # rhino_scopes / rhino_default_scope. Rendered as 403 by ResourcesController.
6
+ class ScopeNotAllowedError < StandardError; end
7
+ end
data/lib/rhino/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Rhino
4
- VERSION = "4.3.0"
4
+ VERSION = "4.4.0"
5
5
  end
data/lib/rhino.rb CHANGED
@@ -3,6 +3,7 @@
3
3
  require "rhino/version"
4
4
  require "rhino/configuration"
5
5
  require "rhino/auth_rejected"
6
+ require "rhino/scope_not_allowed_error"
6
7
  require "rhino/auth_hooks"
7
8
  require "rhino/group_membership"
8
9
  require "rhino/resource_scope"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rhino-rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.3.0
4
+ version: 4.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Bruno Cipolla
@@ -235,6 +235,7 @@ files:
235
235
  - lib/rhino/routes.rb
236
236
  - lib/rhino/routing/domain_constraint.rb
237
237
  - lib/rhino/routing/route_group_validator.rb
238
+ - lib/rhino/scope_not_allowed_error.rb
238
239
  - lib/rhino/tasks/rhino.rake
239
240
  - lib/rhino/templates/audit_trail/create_audit_logs.rb.erb
240
241
  - lib/rhino/templates/generate/factory.rb.erb