class MultiTenant::Middleware

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]]
  }

Constants

DEFAULT_NOT_FOUND

Default Proc for the not_found option

Attributes

globals[RW]

@return [Hash] Global identifiers and their allowed paths and methods

identifier[RW]

@return [Proc] A Proc which accepts a Rack::Request and returns some identifier for tenant lookup

model[RW]

@return [Proc|String|Class] The ActiveRecord model that holds all the tenants

not_found[RW]

@return [Proc] A Proc which accepts a (non-existent or blank) tenant identifier and returns a rack response describing the error. Defaults to a 404 and some shitty html.

Public Class Methods

new(app, opts) click to toggle source

Initialize a new multi tenant Rack middleware.

@param app the Rack app @param opts [Hash] Required: :model, :identifier. Optional: :globals, :not_found.

# 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

Public Instance Methods

call(env) click to toggle source

Rack request call

# 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) click to toggle source
# 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) click to toggle source
# 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) click to toggle source
# 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) click to toggle source
# 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() click to toggle source
# 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