Class: MultiTenant::Middleware
- Inherits:
-
Object
- Object
- MultiTenant::Middleware
- Defined in:
- lib/multi_tenant/middleware.rb
Overview
Rack middleware that sets the current tenant during each request (in a thread-safe manner). During a request, you can access the current tenant from “Tenant.current”, where ‘Tenant’ is name of the ActiveRecord model.
use MultiTenant::Middleware,
# The ActiveRecord model that represents the tenants. Or a Proc returning it, or it's String name.
model: -> { Tenant },
# A Proc that returns the tenant identifier that's used to look up the tenant. (i.e. :using option passed to acts_as_tenant).
# Also aliased as "identifiers".
identifier: ->(req) { req.host.split(/\./)[0] },
# (optional) A Hash of fake identifiers that should be allowed through. Each identifier will have a
# Hash of Regex paths with Symbol http methods (or arrays thereof), or :any. These path & method combos
# will be allowed through when the identifier matches. All others will be blocked.
# IMPORTANT Tenant.current will be nil!
globals: {
"global" => {
%r{\A/api/widgets/} => :any,
%r{\A/api/splines/} => [:get, :post]
}
},
# (optional) Returns a Rack response when a tenant couldn't be found in the db, or when
# a tenant isn't given (and isn't in the `global_paths` list)
not_found: ->(x) {
body = {errors: ["'#{x}' is not a valid tenant. I'm sorry. I'm so sorry."]}.to_json
[400, {'Content-Type' => 'application/json', 'Content-Length' => body.size.to_s}, [body]]
}
Constant Summary collapse
- DEFAULT_NOT_FOUND =
Default Proc for the not_found option
->(x) { body = "<h1>Invalid tenant: #{Array(x).map(&:to_s).join ', '}</h1>" [404, {'Content-Type' => 'text/html', 'Content-Length' => body.size.to_s}, [body]] }
Instance Attribute Summary collapse
-
#globals ⇒ Hash
Global identifiers and their allowed paths and methods.
-
#identifier ⇒ Proc
A Proc which accepts a Rack::Request and returns some identifier for tenant lookup.
-
#model ⇒ Proc|String|Class
The ActiveRecord model that holds all the tenants.
-
#not_found ⇒ Proc
the error.
Instance Method Summary collapse
-
#call(env) ⇒ Object
Rack request call.
- #identifiers(records_or_identifiers) ⇒ Object
-
#initialize(app, opts) ⇒ Middleware
constructor
Initialize a new multi tenant Rack middleware.
- #matching_globals(records_or_identifiers) ⇒ Object
- #path_matches?(req, paths) ⇒ Boolean
- #tenant_class(m = self.model) ⇒ Object
- #tenant_query ⇒ Object
Constructor Details
#initialize(app, opts) ⇒ Middleware
Initialize a new multi tenant Rack middleware.
61 62 63 64 65 66 67 68 69 70 71 72 73 |
# File 'lib/multi_tenant/middleware.rb', line 61 def initialize(app, opts) @app = app self.model = opts.fetch :model self.identifier = opts[:identifier] || opts[:identifiers] || raise("Option :identifier or :identifiers is required") self.globals = (opts[:globals] || {}).reduce({}) { |a, (global, patterns)| a[global] = patterns.reduce({}) { |aa, (path, methods)| aa[path] = methods == :any ? :any : Set.new(Array(methods).map { |m| m.to_s.upcase }) aa } a } self.not_found = opts[:not_found] || DEFAULT_NOT_FOUND end |
Instance Attribute Details
#globals ⇒ Hash
Returns Global identifiers and their allowed paths and methods.
43 44 45 |
# File 'lib/multi_tenant/middleware.rb', line 43 def globals @globals end |
#identifier ⇒ Proc
Returns A Proc which accepts a Rack::Request and returns some identifier for tenant lookup.
40 41 42 |
# File 'lib/multi_tenant/middleware.rb', line 40 def identifier @identifier end |
#model ⇒ Proc|String|Class
Returns The ActiveRecord model that holds all the tenants.
37 38 39 |
# File 'lib/multi_tenant/middleware.rb', line 37 def model @model end |
#not_found ⇒ Proc
the error. Defaults to a 404 and some shitty html.
47 48 49 |
# File 'lib/multi_tenant/middleware.rb', line 47 def not_found @not_found end |
Instance Method Details
#call(env) ⇒ Object
Rack request call
76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 |
# File 'lib/multi_tenant/middleware.rb', line 76 def call(env) tenant_class.current = nil request = Rack::Request.new env id_resp = identifier.(request) records_or_identifiers = Array(id_resp) if (matching = matching_globals(records_or_identifiers)).any? allowed = matching.any? { |allowed_paths| path_matches?(request, allowed_paths) } return @app.call(env) if allowed ids = identifiers records_or_identifiers return not_found.(id_resp.is_a?(Array) ? ids : ids[0]) elsif (tenant_query.current_tenants = records_or_identifiers) and tenant_class.current? return @app.call env else ids = identifiers records_or_identifiers return not_found.(id_resp.is_a?(Array) ? ids : ids[0]) end rescue ::MultiTenant::TenantsNotFound => e ids = e.not_found not_found.(id_resp.is_a?(Array) ? ids : ids[0]) ensure tenant_class.current = nil end |
#identifiers(records_or_identifiers) ⇒ Object
120 121 122 123 124 125 126 127 128 |
# File 'lib/multi_tenant/middleware.rb', line 120 def identifiers(records_or_identifiers) records_or_identifiers.map { |x| if x.class.respond_to?(:table_name) and x.class.table_name == tenant_class.table_name x.send tenant_class.tenant_identifier else x.to_s end } end |
#matching_globals(records_or_identifiers) ⇒ Object
113 114 115 116 117 118 |
# File 'lib/multi_tenant/middleware.rb', line 113 def matching_globals(records_or_identifiers) identifiers(records_or_identifiers).reduce([]) { |a, id| a << globals[id] if globals.has_key? id a } end |
#path_matches?(req, paths) ⇒ Boolean
107 108 109 110 111 |
# File 'lib/multi_tenant/middleware.rb', line 107 def path_matches?(req, paths) paths.any? { |(path, methods)| path === req.path && (methods == :any || methods.include?(req.request_method)) } end |
#tenant_class(m = self.model) ⇒ Object
130 131 132 133 134 135 136 137 138 139 140 |
# File 'lib/multi_tenant/middleware.rb', line 130 def tenant_class(m = self.model) @tenant_class ||= if m.respond_to?(:call) tenant_class m.call elsif m.respond_to? :constantize m.constantize elsif m.respond_to? :model m.model else m end end |
#tenant_query ⇒ Object
142 143 144 145 146 147 148 149 150 |
# File 'lib/multi_tenant/middleware.rb', line 142 def tenant_query @tenant_query ||= if self.model.respond_to?(:call) self.model.call elsif self.model.respond_to? :constantize self.model.constantize else self.model end end |