Class: MultiTenant::Middleware

Inherits:
Object
  • Object
show all
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

Instance Method Summary collapse

Constructor Details

#initialize(app, opts) ⇒ Middleware

Initialize a new multi tenant Rack middleware.

Parameters:

  • app

    the Rack app

  • opts (Hash)

    Required: :model, :identifier. Optional: :globals, :not_found.



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

#globalsHash

Returns Global identifiers and their allowed paths and methods.

Returns:

  • (Hash)

    Global identifiers and their allowed paths and methods



43
44
45
# File 'lib/multi_tenant/middleware.rb', line 43

def globals
  @globals
end

#identifierProc

Returns A Proc which accepts a Rack::Request and returns some identifier for tenant lookup.

Returns:

  • (Proc)

    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

#modelProc|String|Class

Returns The ActiveRecord model that holds all the tenants.

Returns:

  • (Proc|String|Class)

    The ActiveRecord model that holds all the tenants



37
38
39
# File 'lib/multi_tenant/middleware.rb', line 37

def model
  @model
end

#not_foundProc

the error. Defaults to a 404 and some shitty html.

Returns:

  • (Proc)

    A Proc which accepts a (non-existent or blank) tenant identifier and returns a rack response describing



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

Returns:

  • (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_queryObject



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