rhino-rails 4.7.0 → 4.7.3
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 +4 -4
- data/lib/rhino/concerns/route_group_context.rb +59 -0
- data/lib/rhino/configuration.rb +26 -2
- data/lib/rhino/context.rb +41 -1
- data/lib/rhino/engine.rb +1 -0
- data/lib/rhino/missing_tenant_context.rb +10 -1
- data/lib/rhino/query.rb +41 -8
- data/lib/rhino/templates/rhino.rb +22 -0
- data/lib/rhino/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c7f50b9ced877e5d2fc1b304798f7c2c6372e89754ea1c7c9efd296c8f478c33
|
|
4
|
+
data.tar.gz: cca1fbd5f280df7e94a1fa9a69c1ca338fdfff6b76d35158afdc16fd07ef4309
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 43e7ae4d3e346c984ab8452c8dbc2f3ceb7199f35f8e3eba3f971931aed8539e752f9744393f2429125cb94b6d092358c070d719b634fa95b0cd9e314ea362e1
|
|
7
|
+
data.tar.gz: d03e920444bfcdfc0c42ddfa0d494ac180aae4fc499acd852aafc1ca6486aeccaecdff54decf4e64f65e21e5c7cb0f078ea81b3426518d7495b7e8bc3a4bc7de
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rhino
|
|
4
|
+
# Places a custom (non-Rhino) controller into a Rhino route group, so the
|
|
5
|
+
# resource-scope resolver knows whether the request has a tenant boundary.
|
|
6
|
+
#
|
|
7
|
+
# Rhino's own generated controllers already publish their group; a controller
|
|
8
|
+
# you write yourself is outside that chain, so include this concern in it:
|
|
9
|
+
#
|
|
10
|
+
# class Admin::DashboardController < ApplicationController
|
|
11
|
+
# include Rhino::RouteGroupContext
|
|
12
|
+
# rhino_route_group :admin
|
|
13
|
+
#
|
|
14
|
+
# def summary
|
|
15
|
+
# # :admin is declared `tenant: false`, so this spans every organization
|
|
16
|
+
# # instead of raising Rhino::MissingTenantContext.
|
|
17
|
+
# render json: { tasks: Rhino.query(Task).count }
|
|
18
|
+
# end
|
|
19
|
+
# end
|
|
20
|
+
#
|
|
21
|
+
# Without an explicit `rhino_route_group`, the group is read from the route's
|
|
22
|
+
# own `route_group` default:
|
|
23
|
+
#
|
|
24
|
+
# get "/api/admin/dashboard", to: "admin/dashboard#summary",
|
|
25
|
+
# defaults: { route_group: "admin" }
|
|
26
|
+
module RouteGroupContext
|
|
27
|
+
extend ActiveSupport::Concern
|
|
28
|
+
|
|
29
|
+
included do
|
|
30
|
+
# Inheritable, so a base controller can declare the group for a whole
|
|
31
|
+
# namespace and subclasses may override it.
|
|
32
|
+
class_attribute :rhino_route_group_name, instance_writer: false, default: nil
|
|
33
|
+
|
|
34
|
+
before_action :rhino_set_route_group
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
class_methods do
|
|
38
|
+
# Declare the route group this controller's actions belong to.
|
|
39
|
+
def rhino_route_group(name)
|
|
40
|
+
self.rhino_route_group_name = name.to_s
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
# Publish the group for Rhino::Context (and therefore for Rhino.query, the
|
|
47
|
+
# policies and the permission resolver) for the rest of this request.
|
|
48
|
+
def rhino_set_route_group
|
|
49
|
+
return unless defined?(RequestStore)
|
|
50
|
+
|
|
51
|
+
# An explicit declaration wins; otherwise fall back to the route's own
|
|
52
|
+
# `route_group` default, which only exists once a request is bound.
|
|
53
|
+
resolved = rhino_route_group_name.presence
|
|
54
|
+
resolved ||= params[:route_group].presence if request
|
|
55
|
+
|
|
56
|
+
RequestStore.store[:rhino_route_group] = resolved
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
data/lib/rhino/configuration.rb
CHANGED
|
@@ -62,7 +62,15 @@ module Rhino
|
|
|
62
62
|
# captures the subdomain and feeds organization resolution exactly like the
|
|
63
63
|
# path-prefix ":organization" does. Groups without a domain (nil/blank)
|
|
64
64
|
# match any host (default, fully backward compatible).
|
|
65
|
-
|
|
65
|
+
#
|
|
66
|
+
# The optional `tenant:` keyword declares whether the group has a tenant
|
|
67
|
+
# boundary. It defaults to true: Rhino.query inside the group fails closed,
|
|
68
|
+
# raising Rhino::MissingTenantContext when an organization-scopable model is
|
|
69
|
+
# queried with no organization resolved. Pass `tenant: false` for a group
|
|
70
|
+
# that legitimately spans every organization — a back office or admin group
|
|
71
|
+
# whose operators see all tenants' rows.
|
|
72
|
+
def route_group(name, prefix: "", domain: nil, middleware: [], models: :all, auth: false, hooks: nil,
|
|
73
|
+
tenant: true)
|
|
66
74
|
normalized_domain = domain.to_s.strip
|
|
67
75
|
normalized_domain = nil if normalized_domain.empty?
|
|
68
76
|
|
|
@@ -72,10 +80,26 @@ module Rhino
|
|
|
72
80
|
middleware: Array(middleware),
|
|
73
81
|
models: models,
|
|
74
82
|
auth: !!auth,
|
|
75
|
-
hooks: hooks
|
|
83
|
+
hooks: hooks,
|
|
84
|
+
tenant: tenant != false
|
|
76
85
|
}
|
|
77
86
|
end
|
|
78
87
|
|
|
88
|
+
# Whether the named route group has a tenant boundary, i.e. whether
|
|
89
|
+
# Rhino.query must fail closed inside it. Only a group that explicitly
|
|
90
|
+
# declares `tenant: false` does not; every other answer — an unknown group,
|
|
91
|
+
# an untagged route, or no group at all (jobs, rake tasks, console) — is
|
|
92
|
+
# true, so the resolver keeps failing closed wherever the group is not
|
|
93
|
+
# provably non-tenant.
|
|
94
|
+
def group_tenant?(name)
|
|
95
|
+
return true if name.nil? || name.to_s.empty?
|
|
96
|
+
|
|
97
|
+
group = @route_groups[name.to_sym]
|
|
98
|
+
return true unless group
|
|
99
|
+
|
|
100
|
+
group.fetch(:tenant, true) != false
|
|
101
|
+
end
|
|
102
|
+
|
|
79
103
|
# Resolve a model class from its slug
|
|
80
104
|
def resolve_model(slug)
|
|
81
105
|
klass_name = @models[slug.to_sym]
|
data/lib/rhino/context.rb
CHANGED
|
@@ -30,10 +30,24 @@ module Rhino
|
|
|
30
30
|
end
|
|
31
31
|
end
|
|
32
32
|
|
|
33
|
+
# The active route group: the explicit override if one is in effect (set by
|
|
34
|
+
# Rhino.in_route_group), else the group stashed into RequestStore by Rhino's
|
|
35
|
+
# own controllers and by Rhino::RouteGroupContext in custom ones.
|
|
36
|
+
#
|
|
37
|
+
# Unlike user/organization, an override never *erases* the request's group —
|
|
38
|
+
# +with+ only installs a non-nil one — so Rhino.for_user(u).query(M) inside a
|
|
39
|
+
# non-tenant request keeps that request's group.
|
|
40
|
+
def route_group
|
|
41
|
+
value = store[:rhino_route_group]
|
|
42
|
+
value = RequestStore.store[:rhino_route_group] if value.nil? && defined?(RequestStore)
|
|
43
|
+
|
|
44
|
+
value.nil? || value.to_s.empty? ? nil : value.to_s
|
|
45
|
+
end
|
|
46
|
+
|
|
33
47
|
# Run +block+ with the given user/organization installed into RequestStore.
|
|
34
48
|
# Snapshots the prior RequestStore user+org, sets the new ones, yields, and
|
|
35
49
|
# restores the snapshot in an ensure. Returns the block's value.
|
|
36
|
-
def with(user:, organization:)
|
|
50
|
+
def with(user:, organization:, route_group: nil)
|
|
37
51
|
return yield unless defined?(RequestStore)
|
|
38
52
|
|
|
39
53
|
had_user = RequestStore.store.key?(:rhino_current_user)
|
|
@@ -41,6 +55,13 @@ module Rhino
|
|
|
41
55
|
prev_user = RequestStore.store[:rhino_current_user]
|
|
42
56
|
prev_org = RequestStore.store[:rhino_organization]
|
|
43
57
|
|
|
58
|
+
# A route group is only ever ADDED, never cleared: with no explicit group
|
|
59
|
+
# the request's own one (if any) stays in effect.
|
|
60
|
+
had_group = RequestStore.store.key?(:rhino_route_group)
|
|
61
|
+
prev_group = RequestStore.store[:rhino_route_group]
|
|
62
|
+
had_override_group = store.key?(:rhino_route_group)
|
|
63
|
+
prev_override_group = store[:rhino_route_group]
|
|
64
|
+
|
|
44
65
|
# Track the active override so Context.user/organization prefer it even when
|
|
45
66
|
# the passed value is nil (distinguishing "explicitly nil" from "absent").
|
|
46
67
|
had_override_user = store.key?(:rhino_current_user)
|
|
@@ -53,6 +74,11 @@ module Rhino
|
|
|
53
74
|
store[:rhino_current_user] = user
|
|
54
75
|
store[:rhino_organization] = organization
|
|
55
76
|
|
|
77
|
+
if route_group
|
|
78
|
+
RequestStore.store[:rhino_route_group] = route_group.to_s
|
|
79
|
+
store[:rhino_route_group] = route_group.to_s
|
|
80
|
+
end
|
|
81
|
+
|
|
56
82
|
begin
|
|
57
83
|
yield
|
|
58
84
|
ensure
|
|
@@ -77,6 +103,20 @@ module Rhino
|
|
|
77
103
|
else
|
|
78
104
|
store.delete(:rhino_organization)
|
|
79
105
|
end
|
|
106
|
+
|
|
107
|
+
if route_group
|
|
108
|
+
if had_group
|
|
109
|
+
RequestStore.store[:rhino_route_group] = prev_group
|
|
110
|
+
else
|
|
111
|
+
RequestStore.store.delete(:rhino_route_group)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
if had_override_group
|
|
115
|
+
store[:rhino_route_group] = prev_override_group
|
|
116
|
+
else
|
|
117
|
+
store.delete(:rhino_route_group)
|
|
118
|
+
end
|
|
119
|
+
end
|
|
80
120
|
end
|
|
81
121
|
end
|
|
82
122
|
|
data/lib/rhino/engine.rb
CHANGED
|
@@ -4,5 +4,14 @@ module Rhino
|
|
|
4
4
|
# Raised by Rhino.query (and the explicit builder) when an organization-scopable
|
|
5
5
|
# model is queried with no organization context available. Fail-closed: the
|
|
6
6
|
# resolver never returns an unscoped relation for a tenant-scopable model.
|
|
7
|
-
class MissingTenantContext < StandardError
|
|
7
|
+
class MissingTenantContext < StandardError
|
|
8
|
+
def initialize(model_class = nil)
|
|
9
|
+
super(
|
|
10
|
+
"Rhino.query(#{model_class}) requires an organization context but none is set. " \
|
|
11
|
+
"Use Rhino.for_user(...).in_organization(...) outside a tenant request, or declare " \
|
|
12
|
+
"the route group serving this request non-tenant with `tenant: false` if it " \
|
|
13
|
+
"legitimately spans every organization."
|
|
14
|
+
)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
8
17
|
end
|
data/lib/rhino/query.rb
CHANGED
|
@@ -20,7 +20,11 @@ module Rhino
|
|
|
20
20
|
# (BelongsToOrganization / HasAutoScope read RequestStore at BUILD time).
|
|
21
21
|
#
|
|
22
22
|
# Fail closed: an org-scopable model with no org context RAISES
|
|
23
|
-
# Rhino::MissingTenantContext rather than returning an unscoped relation
|
|
23
|
+
# Rhino::MissingTenantContext rather than returning an unscoped relation —
|
|
24
|
+
# unless the query belongs to a route group declared non-tenant
|
|
25
|
+
# (`tenant: false`), either because the request is served by that group or
|
|
26
|
+
# because the caller said so with Rhino.in_route_group(...). An explicit
|
|
27
|
+
# organization is always honored, in every group.
|
|
24
28
|
def query(model_class)
|
|
25
29
|
org = Rhino::Context.organization
|
|
26
30
|
|
|
@@ -28,9 +32,11 @@ module Rhino
|
|
|
28
32
|
relation = model_class.all
|
|
29
33
|
|
|
30
34
|
if Rhino::ScopesToOrganization.organization_scoped?(model_class)
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
35
|
+
if org
|
|
36
|
+
relation = Rhino::ScopesToOrganization.scope_to_organization(relation, model_class, org, strict: true)
|
|
37
|
+
elsif Rhino.config.group_tenant?(Rhino::Context.route_group)
|
|
38
|
+
raise Rhino::MissingTenantContext, model_class.name
|
|
39
|
+
end
|
|
34
40
|
end
|
|
35
41
|
|
|
36
42
|
relation
|
|
@@ -48,6 +54,20 @@ module Rhino
|
|
|
48
54
|
Rhino::PendingScopedContext.new(user: user)
|
|
49
55
|
end
|
|
50
56
|
|
|
57
|
+
# Begin an explicit context that acts as the named route group, for use
|
|
58
|
+
# where no request resolves one — an Active Job, a rake task, the console,
|
|
59
|
+
# a test.
|
|
60
|
+
#
|
|
61
|
+
# The group's own configuration still decides the boundary: naming a group
|
|
62
|
+
# declared +tenant: false+ lets the query span every organization, while
|
|
63
|
+
# naming any other group keeps failing closed.
|
|
64
|
+
#
|
|
65
|
+
# Rhino.in_route_group(:admin).query(Task)
|
|
66
|
+
# Rhino.for_user(user).in_route_group(:admin).run { ... }
|
|
67
|
+
def in_route_group(route_group)
|
|
68
|
+
Rhino::PendingScopedContext.new(user: nil, route_group: route_group)
|
|
69
|
+
end
|
|
70
|
+
|
|
51
71
|
# The ambient context resolver.
|
|
52
72
|
def context
|
|
53
73
|
Rhino::Context
|
|
@@ -84,9 +104,10 @@ module Rhino
|
|
|
84
104
|
# Fluent explicit-context builder. Holds a user (and, once chained, an org) and
|
|
85
105
|
# resolves queries with that context installed into RequestStore at build time.
|
|
86
106
|
class PendingScopedContext
|
|
87
|
-
def initialize(user:, organization: nil)
|
|
107
|
+
def initialize(user:, organization: nil, route_group: nil)
|
|
88
108
|
@user = user
|
|
89
109
|
@organization = organization
|
|
110
|
+
@route_group = route_group
|
|
90
111
|
end
|
|
91
112
|
|
|
92
113
|
def in_organization(organization)
|
|
@@ -94,6 +115,18 @@ module Rhino
|
|
|
94
115
|
self
|
|
95
116
|
end
|
|
96
117
|
|
|
118
|
+
# Act as the named route group for this context (see Rhino.in_route_group).
|
|
119
|
+
def in_route_group(route_group)
|
|
120
|
+
@route_group = route_group
|
|
121
|
+
self
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Set the explicit user for this context.
|
|
125
|
+
def for_user(user)
|
|
126
|
+
@user = user
|
|
127
|
+
self
|
|
128
|
+
end
|
|
129
|
+
|
|
97
130
|
# Build a fully-baked relation for +model_class+ under this explicit context.
|
|
98
131
|
#
|
|
99
132
|
# Because Rails default_scopes bake at BUILD time, we install the user+org into
|
|
@@ -101,14 +134,14 @@ module Rhino
|
|
|
101
134
|
# The org+user are baked into the returned relation — no stickiness, fully
|
|
102
135
|
# isolated: a later Rhino.query with no context still fails closed.
|
|
103
136
|
def query(model_class)
|
|
104
|
-
Rhino::Context.with(user: @user, organization: @organization) do
|
|
137
|
+
Rhino::Context.with(user: @user, organization: @organization, route_group: @route_group) do
|
|
105
138
|
Rhino.query(model_class)
|
|
106
139
|
end
|
|
107
140
|
end
|
|
108
141
|
|
|
109
142
|
# Build a fully-baked, named-scoped relation under this explicit context.
|
|
110
143
|
def scoped_query(model_class, scope_name = nil)
|
|
111
|
-
Rhino::Context.with(user: @user, organization: @organization) do
|
|
144
|
+
Rhino::Context.with(user: @user, organization: @organization, route_group: @route_group) do
|
|
112
145
|
Rhino.scoped_query(model_class, scope_name)
|
|
113
146
|
end
|
|
114
147
|
end
|
|
@@ -117,7 +150,7 @@ module Rhino
|
|
|
117
150
|
# inside the block see the context; RequestStore is restored afterward.
|
|
118
151
|
# Returns the block's value.
|
|
119
152
|
def run(&block)
|
|
120
|
-
Rhino::Context.with(user: @user, organization: @organization, &block)
|
|
153
|
+
Rhino::Context.with(user: @user, organization: @organization, route_group: @route_group, &block)
|
|
121
154
|
end
|
|
122
155
|
end
|
|
123
156
|
end
|
|
@@ -44,6 +44,28 @@ Rhino.configure do |config|
|
|
|
44
44
|
# the path prefix ':organization' does. Groups without a domain match any host.
|
|
45
45
|
# config.route_group :admin, prefix: '', domain: 'admin.example.com', models: :all
|
|
46
46
|
# config.route_group :tenant, prefix: '', domain: '{organization}.example.com', models: :all
|
|
47
|
+
#
|
|
48
|
+
# The optional `tenant:` keyword declares whether a group has a tenant
|
|
49
|
+
# boundary. It defaults to true: Rhino.query inside the group fails closed,
|
|
50
|
+
# raising Rhino::MissingTenantContext when an organization-scopable model is
|
|
51
|
+
# queried with no organization resolved. Pass `tenant: false` for a group that
|
|
52
|
+
# legitimately spans every organization — a back office whose operators see all
|
|
53
|
+
# tenants' rows. There, Rhino.query applies no organization filter and does not
|
|
54
|
+
# raise; the models' own scopes and the policies still apply, and an explicit
|
|
55
|
+
# Rhino.for_user(u).in_organization(org) still scopes.
|
|
56
|
+
# config.route_group :admin, prefix: 'admin', tenant: false, models: :all
|
|
57
|
+
#
|
|
58
|
+
# Custom (non-Rhino) controllers publish their group by including
|
|
59
|
+
# Rhino::RouteGroupContext and declaring `rhino_route_group :admin`, or by
|
|
60
|
+
# tagging the route with `defaults: { route_group: 'admin' }`. Outside a
|
|
61
|
+
# request (jobs, rake tasks, console) no group resolves, so the caller names
|
|
62
|
+
# the one it is acting as:
|
|
63
|
+
#
|
|
64
|
+
# Rhino.in_route_group(:admin).query(Task)
|
|
65
|
+
# Rhino.for_user(operator).in_route_group(:admin).run { ... }
|
|
66
|
+
#
|
|
67
|
+
# The group's own `tenant:` still decides: naming a tenant group there changes
|
|
68
|
+
# nothing, the query still fails closed.
|
|
47
69
|
|
|
48
70
|
# config.route_group :default, prefix: '', middleware: [], models: :all
|
|
49
71
|
|
data/lib/rhino/version.rb
CHANGED
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.7.
|
|
4
|
+
version: 4.7.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Bruno Cipolla
|
|
@@ -215,6 +215,7 @@ files:
|
|
|
215
215
|
- lib/rhino/concerns/has_uuid.rb
|
|
216
216
|
- lib/rhino/concerns/has_validation.rb
|
|
217
217
|
- lib/rhino/concerns/hidable_columns.rb
|
|
218
|
+
- lib/rhino/concerns/route_group_context.rb
|
|
218
219
|
- lib/rhino/configuration.rb
|
|
219
220
|
- lib/rhino/context.rb
|
|
220
221
|
- lib/rhino/controllers/auth_controller.rb
|