rack-multitenant 1.0.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 +7 -0
- data/.gitignore +19 -0
- data/Gemfile +19 -0
- data/LICENSE.txt +22 -0
- data/README.md +29 -0
- data/Rakefile +18 -0
- data/examples/example_helper.rb +9 -0
- data/examples/get_identity.rb +38 -0
- data/examples/get_identity_spec.rb +34 -0
- data/examples/getter_strategies.rb +31 -0
- data/examples/getter_strategies_spec.rb +22 -0
- data/examples/multitenant_sinatra.rb +169 -0
- data/lib/rack-multitenant.rb +3 -0
- data/lib/rack-multitenant/version.rb +5 -0
- data/lib/rack/multitenant.rb +13 -0
- data/lib/rack/multitenant/get_current_tenant.rb +98 -0
- data/lib/rack/multitenant/get_identity.rb +45 -0
- data/lib/rack/multitenant/tenant_strategies/default.rb +18 -0
- data/lib/rack/multitenant/tenant_strategies/env_variable.rb +22 -0
- data/lib/rack/multitenant/tenant_strategies/port.rb +19 -0
- data/lib/rack/multitenant/tenant_strategies/subdomain.rb +40 -0
- data/rack-multitenant.gemspec +21 -0
- metadata +88 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 8668da451467bac3c7fd343928512230aa796c3a
|
4
|
+
data.tar.gz: 93aa7f15e84a84509103edb89555bae8a877eb64
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 996ae7d0d9c6aaa00cbe5044b5955005e8c6f8a3a5f4c62af49573a86b04d6c5dbf537d48bcef60eacd39d5a4649bd7d5998db6e9f5ccf1eab6b96bffd092f05
|
7
|
+
data.tar.gz: bf5fe47655d2546955fa1c0136898146545e8b820857972aa4cf65b36d4f28ccb96fc67382f6bcaebd916c7e0bc8760a6d7d7adffa333fa69327f479a431f426
|
data/.gitignore
ADDED
data/Gemfile
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
source 'https://rubygems.org'
|
2
|
+
|
3
|
+
# Specify your gem's dependencies in rack-multitenant.gemspec
|
4
|
+
gemspec
|
5
|
+
|
6
|
+
group :development, :test do
|
7
|
+
gem "rake", "~> 10.0.3"
|
8
|
+
gem "pry", "~> 0.9.11"
|
9
|
+
|
10
|
+
gem "activerecord"
|
11
|
+
gem "sinatra"
|
12
|
+
gem "haml"
|
13
|
+
gem "sqlite3"
|
14
|
+
end
|
15
|
+
|
16
|
+
group :test do
|
17
|
+
gem "rdoctest", "~> 0.0.2"
|
18
|
+
gem "rack-test", "~> 0.6.2"
|
19
|
+
end
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 Andrew O'Brien
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# Rack::Multitenant
|
2
|
+
|
3
|
+
## What?
|
4
|
+
|
5
|
+
Many web-applications want to provide a sandboxed instance for each customer, but know that this would be impractical and inefficient. To present the illusion of this, they often provide a subdomain, url prefix, different domain entirely, or something more exotic. Many of these can hamper development or make it hard to reproduce production problems.
|
6
|
+
|
7
|
+
Rack::Multitenant provides an interface to allow the application to decide which customer (or _tenant_) the request the for. It will assign the tenant object to the request's environment hash, meaning that all Rack compliant applications will be able to use it.
|
8
|
+
|
9
|
+
## Installation
|
10
|
+
|
11
|
+
Add this line to your application's Gemfile:
|
12
|
+
|
13
|
+
gem 'rack-multitenant'
|
14
|
+
|
15
|
+
And then execute:
|
16
|
+
|
17
|
+
$ bundle
|
18
|
+
|
19
|
+
Or install it yourself as:
|
20
|
+
|
21
|
+
$ gem install rack-multitenant
|
22
|
+
|
23
|
+
## Contributing
|
24
|
+
|
25
|
+
1. Fork it
|
26
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
27
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
28
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
29
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
2
|
+
require 'rdoctest/task'
|
3
|
+
require 'rake/testtask'
|
4
|
+
|
5
|
+
namespace :test do
|
6
|
+
Rdoctest::Task.new :unit do |t|
|
7
|
+
#t.libs << 'lib' # The 'lib' directory is loaded by default,
|
8
|
+
t.pattern = 'lib/rack/multitenant/**/*.rb'
|
9
|
+
end
|
10
|
+
Rake::TestTask.new :integration do |t|
|
11
|
+
t.libs << "lib"
|
12
|
+
t.test_files = FileList['examples/*_spec.rb']
|
13
|
+
#t.verbose = true
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
desc "Run unit and integration tests"
|
18
|
+
task :test => %w{test:unit test:integration}
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require "rack"
|
2
|
+
require "rack/builder"
|
3
|
+
require "rack/auth/basic"
|
4
|
+
require "rack/multitenant"
|
5
|
+
|
6
|
+
ACCOUNTS = {
|
7
|
+
andrew: {
|
8
|
+
foo: :user
|
9
|
+
},
|
10
|
+
vince: {
|
11
|
+
foo: :admin,
|
12
|
+
bar: :user
|
13
|
+
},
|
14
|
+
lou: {}
|
15
|
+
}
|
16
|
+
|
17
|
+
Get_identity = Rack::Builder.new do
|
18
|
+
use Rack::MultiTenant::GetCurrentTenant do |request|
|
19
|
+
# TODO: subdomain strategy for getting current tenant.
|
20
|
+
request.url =~ /foo/ ? :foo : :bar
|
21
|
+
end
|
22
|
+
# TODO: replace with Warden
|
23
|
+
use Rack::Auth::Basic do |username, password|
|
24
|
+
username if password == "password" && accounts = ACCOUNTS[username.to_sym]
|
25
|
+
end
|
26
|
+
use Rack::MultiTenant::GetIdentity do |username, tenant|
|
27
|
+
# TODO: DB lookup
|
28
|
+
if identities = ACCOUNTS[username.to_sym] and identity = identities[tenant]
|
29
|
+
->(app) { app.pass identity } # Or just return identity.
|
30
|
+
else
|
31
|
+
->(app) { app.forbid! }
|
32
|
+
# -> (app) { app.create_with SignUpWithExisting, identities } or something like this...
|
33
|
+
end
|
34
|
+
end
|
35
|
+
run lambda {|env|
|
36
|
+
[200, {}, "Your identity: #{env['rack.multitenant.identity']}"]
|
37
|
+
}
|
38
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require_relative "example_helper"
|
2
|
+
|
3
|
+
describe "Simple example" do
|
4
|
+
def app
|
5
|
+
@app = Rack::Builder.parse_file('./examples/get_identity.rb').first
|
6
|
+
end
|
7
|
+
|
8
|
+
it "gets the identity for an authorized user of the given tenant" do
|
9
|
+
authorize "andrew", "password"
|
10
|
+
get "/foo"
|
11
|
+
assert last_response.ok?
|
12
|
+
assert_equal("Your identity: user", last_response.body)
|
13
|
+
end
|
14
|
+
|
15
|
+
it "does not allow unauthenticated users" do
|
16
|
+
get "/foo"
|
17
|
+
assert_equal 401, last_response.status
|
18
|
+
|
19
|
+
authorize "andrew", "fail"
|
20
|
+
get "/foo"
|
21
|
+
assert_equal 401, last_response.status
|
22
|
+
end
|
23
|
+
|
24
|
+
it "does not allow in users without an identity for the tenant" do
|
25
|
+
authorize "lou", "password"
|
26
|
+
get "/foo"
|
27
|
+
assert_equal 403, last_response.status
|
28
|
+
|
29
|
+
authorize "andrew", "password"
|
30
|
+
get "/bar"
|
31
|
+
assert_equal 403, last_response.status
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require "rack"
|
2
|
+
require "rack/builder"
|
3
|
+
require "rack/auth/basic"
|
4
|
+
require "rack/multitenant"
|
5
|
+
|
6
|
+
Getter_strategies = Rack::Builder.new do
|
7
|
+
# Builds a piece of middleware that:
|
8
|
+
# 1. First tries to get the tenant from a subdomain
|
9
|
+
# 2. If the RACK_ENV is development or test, it uses a hash to
|
10
|
+
# look up by port.
|
11
|
+
# 3. Last, if a tenant still hasn't been found, it defaults to a
|
12
|
+
# hard-coded value.
|
13
|
+
#
|
14
|
+
# The return value of the block will be saved as the value of
|
15
|
+
# <tt>env['rack.multitenant.current_tenant']</tt>.
|
16
|
+
use *Rack::MultiTenant::GetCurrentTenant.build {|builder|
|
17
|
+
builder.use :Subdomain, "example.com"
|
18
|
+
#builder.use :PathPrefix Not implemented. Anyone need this?
|
19
|
+
if %w{development test}.include?(ENV["RACK_ENV"])
|
20
|
+
builder.use :Port, {
|
21
|
+
3000 => :foo,
|
22
|
+
3001 => :bar
|
23
|
+
}
|
24
|
+
#builder.use :EnvVariable
|
25
|
+
end
|
26
|
+
builder.use :Default, :bar
|
27
|
+
}
|
28
|
+
run lambda {|env|
|
29
|
+
[200, {}, "Current tenant: #{env['rack.multitenant.current_tenant']}"]
|
30
|
+
}
|
31
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require_relative "example_helper"
|
2
|
+
|
3
|
+
describe "Getter strategies example" do
|
4
|
+
def app
|
5
|
+
@app = Rack::Builder.parse_file('./examples/getter_strategies.rb').first
|
6
|
+
end
|
7
|
+
|
8
|
+
it "gets default tenant" do
|
9
|
+
get "/"
|
10
|
+
|
11
|
+
assert last_response.ok?
|
12
|
+
assert_match /bar/, last_response.body
|
13
|
+
end
|
14
|
+
|
15
|
+
it "gets tenant by subdomain" do
|
16
|
+
get "/", {}, "HTTP_HOST" => "foo.example.com"
|
17
|
+
|
18
|
+
assert last_response.ok?
|
19
|
+
assert_match /foo/, last_response.body
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
@@ -0,0 +1,169 @@
|
|
1
|
+
# Run with: bundle exec rackup examples/multitenant_sinatra.rb
|
2
|
+
|
3
|
+
$:.unshift(File.dirname(__FILE__) + "/../lib")
|
4
|
+
require "rubygems"
|
5
|
+
require "active_record"
|
6
|
+
require "sqlite3"
|
7
|
+
require "sinatra"
|
8
|
+
require "haml"
|
9
|
+
require "rack/builder"
|
10
|
+
require "rack/auth/basic"
|
11
|
+
require "rack/multitenant"
|
12
|
+
require "pry"
|
13
|
+
|
14
|
+
def connect_to_db!
|
15
|
+
ActiveRecord::Base.establish_connection(
|
16
|
+
adapter: "sqlite3",
|
17
|
+
database: File.dirname(__FILE__) + "/../tmp/multitenant_sinatra.sqlite3"
|
18
|
+
)
|
19
|
+
end
|
20
|
+
connect_to_db!
|
21
|
+
|
22
|
+
ActiveRecord::Migration.create_table :items, force: true do |t|
|
23
|
+
t.text :text
|
24
|
+
t.integer :tenant_id
|
25
|
+
t.integer :created_by_id
|
26
|
+
t.integer :finished_by_id
|
27
|
+
t.timestamps
|
28
|
+
end
|
29
|
+
|
30
|
+
ActiveRecord::Migration.create_table :identities, force: true do |t|
|
31
|
+
t.integer :tenant_id
|
32
|
+
t.integer :account_id
|
33
|
+
end
|
34
|
+
|
35
|
+
ActiveRecord::Migration.create_table :accounts, force: true do |t|
|
36
|
+
t.string :username
|
37
|
+
end
|
38
|
+
|
39
|
+
ActiveRecord::Migration.create_table :tenants, force: true do |t|
|
40
|
+
t.string :key
|
41
|
+
end
|
42
|
+
|
43
|
+
class Item < ActiveRecord::Base
|
44
|
+
belongs_to :created_by, class_name: "Identity"
|
45
|
+
belongs_to :finished_by, class_name: "Identity"
|
46
|
+
belongs_to :tenant
|
47
|
+
|
48
|
+
validates :tenant, presence: true
|
49
|
+
|
50
|
+
def self.add(attrs, creator)
|
51
|
+
create(attrs.merge(created_by: creator))
|
52
|
+
end
|
53
|
+
|
54
|
+
def finish!(finisher)
|
55
|
+
update_attributes(finished_by: finisher)
|
56
|
+
end
|
57
|
+
|
58
|
+
def finished?
|
59
|
+
finished_by.present?
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
class Tenant < ActiveRecord::Base
|
64
|
+
has_many :todos, class_name: "Item"
|
65
|
+
end
|
66
|
+
|
67
|
+
class Identity < ActiveRecord::Base
|
68
|
+
belongs_to :account
|
69
|
+
belongs_to :tenant
|
70
|
+
end
|
71
|
+
|
72
|
+
class Account < ActiveRecord::Base
|
73
|
+
has_many :identities
|
74
|
+
end
|
75
|
+
|
76
|
+
foo_corp = Tenant.create!(key: "foo")
|
77
|
+
bar_inc = Tenant.create!(key: "bar")
|
78
|
+
|
79
|
+
andrew = Account.create!(username: "andrew")
|
80
|
+
vince = Account.create!(username: "vince")
|
81
|
+
lou = Account.create!(username: "lou")
|
82
|
+
|
83
|
+
andrew.identities.create!(tenant: foo_corp)
|
84
|
+
andrew.identities.create!(tenant: bar_inc)
|
85
|
+
vince.identities.create!(tenant: bar_inc)
|
86
|
+
|
87
|
+
class HostedTodos < Sinatra::Base
|
88
|
+
get "/" do
|
89
|
+
redirect "/items"
|
90
|
+
end
|
91
|
+
|
92
|
+
post "/items" do
|
93
|
+
tenant.todos.add(params[:item], current_user)
|
94
|
+
redirect "/items"
|
95
|
+
end
|
96
|
+
|
97
|
+
get "/items" do
|
98
|
+
@items = tenant.todos.all
|
99
|
+
haml :items
|
100
|
+
end
|
101
|
+
|
102
|
+
post "/items/:id/finish" do
|
103
|
+
tenant.todos.find(params[:id]).finish!(current_user)
|
104
|
+
redirect "/items"
|
105
|
+
end
|
106
|
+
|
107
|
+
private
|
108
|
+
def tenant
|
109
|
+
env["rack.multitenant.current_tenant"]
|
110
|
+
end
|
111
|
+
|
112
|
+
def current_user
|
113
|
+
env['rack.multitenant.identity']
|
114
|
+
end
|
115
|
+
|
116
|
+
enable :inline_templates
|
117
|
+
end
|
118
|
+
|
119
|
+
Multitenant_sinatra = Rack::Builder.new do
|
120
|
+
connect_to_db!
|
121
|
+
# Liking this *build method a little less... Probably want to reconsider before releasing.
|
122
|
+
# Just an aesthetic problem--not worth holding things up.
|
123
|
+
use *Rack::MultiTenant::GetCurrentTenant.build {|builder|
|
124
|
+
builder.use :Subdomain, "example.com" do |key|
|
125
|
+
Tenant.find_by_key(key)
|
126
|
+
end
|
127
|
+
builder.use :Default, bar_inc
|
128
|
+
}
|
129
|
+
use Rack::Auth::Basic do |username, password|
|
130
|
+
# In a real example, we could do a couple of things:
|
131
|
+
#
|
132
|
+
# * Multi-identity, single password: store the auth credentials on the Account
|
133
|
+
# * Multi-identity, multi-auth strategy: Move the credentials to the Identity.
|
134
|
+
# Scope your find to the current tenant.
|
135
|
+
password == "password" && account = Account.find_by_username(username.to_sym)
|
136
|
+
end
|
137
|
+
use Rack::MultiTenant::GetIdentity do |account_name, tenant|
|
138
|
+
# Rack::Basic only keeps the username. In a real app, we'd use something that only
|
139
|
+
# made us hit the Account table once per request (like Warden).
|
140
|
+
account = Account.find_by_username(account_name)
|
141
|
+
Identity.where(account_id: account.id, tenant_id: tenant.id).first
|
142
|
+
end
|
143
|
+
run HostedTodos
|
144
|
+
end
|
145
|
+
|
146
|
+
__END__
|
147
|
+
|
148
|
+
@@ layout
|
149
|
+
%html
|
150
|
+
= yield
|
151
|
+
|
152
|
+
@@ items
|
153
|
+
%form(action="/items" method="POST")
|
154
|
+
%p
|
155
|
+
%label(for="text") Text:
|
156
|
+
%input#text(type="text" name="item[text]")
|
157
|
+
%p
|
158
|
+
%input(type="submit" value="Create")
|
159
|
+
|
160
|
+
.items
|
161
|
+
- @items.each do |item|
|
162
|
+
%p
|
163
|
+
- if item.finished?
|
164
|
+
%del
|
165
|
+
= item.text
|
166
|
+
- else
|
167
|
+
= item.text
|
168
|
+
%form{:action => "/items/#{item.id}/finish", :method => "POST"}
|
169
|
+
%input(type="submit" value="Finish")
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module Rack
|
2
|
+
module MultiTenant
|
3
|
+
autoload :GetCurrentTenant, "rack/multitenant/get_current_tenant"
|
4
|
+
autoload :GetIdentity, "rack/multitenant/get_identity"
|
5
|
+
|
6
|
+
module TenantStrategies
|
7
|
+
autoload :Subdomain, "rack/multitenant/tenant_strategies/subdomain"
|
8
|
+
autoload :Port, "rack/multitenant/tenant_strategies/port"
|
9
|
+
autoload :EnvVariable, "rack/multitenant/tenant_strategies/env_variable"
|
10
|
+
autoload :Default, "rack/multitenant/tenant_strategies/default"
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
require_relative "../multitenant"
|
2
|
+
require "rack/request"
|
3
|
+
|
4
|
+
module Rack::MultiTenant
|
5
|
+
class GetCurrentTenant
|
6
|
+
# Convenience method for initializing the GetCurrentTenant middleware
|
7
|
+
# with a stack of strategies.
|
8
|
+
#
|
9
|
+
# >> GetCurrentTenant = Rack::MultiTenant::GetCurrentTenant
|
10
|
+
# A custom getter that will be instantiated once and called.
|
11
|
+
# >> class MyCustomGetter
|
12
|
+
# >> def initialize(host)
|
13
|
+
# >> @host = host
|
14
|
+
# >> end
|
15
|
+
# >> def call(req)
|
16
|
+
# >> req.host == @host ? :custom : nil
|
17
|
+
# >> end
|
18
|
+
# >> end
|
19
|
+
#
|
20
|
+
# >> # Stub the rest of the Rack stack to return the env value we're
|
21
|
+
# >> # interested in.
|
22
|
+
# >> next_app = lambda {|env| env["rack.multitenant.current_tenant"]}
|
23
|
+
#
|
24
|
+
# >> # Build the middleware and instantiate it.
|
25
|
+
# >> app = GetCurrentTenant.new(next_app, GetCurrentTenant.build do |get|
|
26
|
+
# >> get.use :Subdomain, "example.com", &:to_sym
|
27
|
+
# >> get.use MyCustomGetter, "custom"
|
28
|
+
# >> get.use lambda {|req| req.host == "lambda" ? :lambda : nil }
|
29
|
+
# >> get.use :Default, :default_tenant
|
30
|
+
# >> end[1])
|
31
|
+
#
|
32
|
+
# >> subdomain_req_env = {"HTTP_HOST" => "foo.example.com"}
|
33
|
+
# >> app.call(subdomain_req_env)
|
34
|
+
# => :foo
|
35
|
+
#
|
36
|
+
# >> custom_req_env = {"HTTP_HOST" => "custom"}
|
37
|
+
# >> app.call(custom_req_env)
|
38
|
+
# => :custom
|
39
|
+
#
|
40
|
+
# >> lambda_req_env = {"HTTP_HOST" => "lambda"}
|
41
|
+
# >> app.call(lambda_req_env)
|
42
|
+
# => :lambda
|
43
|
+
#
|
44
|
+
# >> default_req_env = {"HTTP_HOST" => "kwjibo"}
|
45
|
+
# >> app.call(default_req_env)
|
46
|
+
# => :default_tenant
|
47
|
+
def self.build
|
48
|
+
[self, (Builder.new.tap {|b| yield b}).to_proc]
|
49
|
+
end
|
50
|
+
|
51
|
+
def initialize(app, foo = nil, &getter)
|
52
|
+
@app, @getter = app, foo || getter
|
53
|
+
end
|
54
|
+
|
55
|
+
def call(env)
|
56
|
+
if tenant = @getter.call(Rack::Request.new(env))
|
57
|
+
env["rack.multitenant.current_tenant"] = tenant
|
58
|
+
end
|
59
|
+
@app.call(env)
|
60
|
+
end
|
61
|
+
|
62
|
+
class Builder
|
63
|
+
def initialize
|
64
|
+
@stack = []
|
65
|
+
end
|
66
|
+
|
67
|
+
def use(name, *args, &blk)
|
68
|
+
@stack << case strategy = resolve(name)
|
69
|
+
when Class
|
70
|
+
strategy.new(*args, &blk)
|
71
|
+
else
|
72
|
+
strategy
|
73
|
+
end
|
74
|
+
end
|
75
|
+
alias_method :with, :use
|
76
|
+
|
77
|
+
def to_proc
|
78
|
+
_stack = @stack.compact
|
79
|
+
lambda {|request|
|
80
|
+
tenant = nil
|
81
|
+
_stack.find {|strategy| tenant = strategy.call(request)}
|
82
|
+
tenant
|
83
|
+
}
|
84
|
+
end
|
85
|
+
|
86
|
+
private
|
87
|
+
def resolve(name)
|
88
|
+
case name
|
89
|
+
when Symbol
|
90
|
+
Rack::MultiTenant::TenantStrategies.const_get(name)
|
91
|
+
else
|
92
|
+
name
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module Rack::MultiTenant
|
2
|
+
class GetIdentity
|
3
|
+
def initialize(app, user_key = "REMOTE_USER", &getter)
|
4
|
+
@app, @user_key, @getter = app, user_key, getter
|
5
|
+
end
|
6
|
+
|
7
|
+
def call(env)
|
8
|
+
user, tenant = env.values_at(@user_key, "rack.multitenant.current_tenant")
|
9
|
+
Next.new(@app, env).call(@getter.call(user, tenant))
|
10
|
+
end
|
11
|
+
|
12
|
+
class Next
|
13
|
+
def initialize(app, env)
|
14
|
+
@app, @env = app, env
|
15
|
+
end
|
16
|
+
|
17
|
+
def call(result)
|
18
|
+
case result
|
19
|
+
when Proc
|
20
|
+
result.call(self)
|
21
|
+
when nil
|
22
|
+
forbid!
|
23
|
+
when false
|
24
|
+
forbid!
|
25
|
+
else
|
26
|
+
pass result
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def pass(identity)
|
31
|
+
@env["rack.multitenant.identity"] = identity
|
32
|
+
@app.call(@env)
|
33
|
+
end
|
34
|
+
|
35
|
+
def forbid!(forbidden_app = nil)
|
36
|
+
[403, {"Content-Type" => "text/html"},
|
37
|
+
"No identity found for #{@env['rack.multitenant.current_tenant']}"]
|
38
|
+
end
|
39
|
+
|
40
|
+
def create_identity_with(subapp)
|
41
|
+
# TODO: calls subapp that either creates or gets the user on the path to creating a new identity.
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require "rack/multitenant"
|
2
|
+
|
3
|
+
module Rack::MultiTenant::TenantStrategies
|
4
|
+
# Always returns the intial value for tenant.
|
5
|
+
#
|
6
|
+
# >> default = Rack::MultiTenant::TenantStrategies::Default.new(:foo)
|
7
|
+
# >> default.call(:stub_request)
|
8
|
+
# => :foo
|
9
|
+
class Default
|
10
|
+
def initialize(default)
|
11
|
+
@default = default
|
12
|
+
end
|
13
|
+
|
14
|
+
def call(_)
|
15
|
+
@default
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Rack::MultiTenant::TenantStrategies
|
2
|
+
class EnvVariable
|
3
|
+
# env: Environment variable containing the tenant or tenant key.
|
4
|
+
# &getter: optional block. If specified, the env variable will be passed in.
|
5
|
+
#
|
6
|
+
# >> ENV["TENANT"] = "foo"
|
7
|
+
# >> s = Rack::MultiTenant::TenantStrategies::EnvVariable.new
|
8
|
+
# >> s.call(:stub_request)
|
9
|
+
# => "foo"
|
10
|
+
#
|
11
|
+
# >> s2 = Rack::MultiTenant::TenantStrategies::EnvVariable.new(&:to_sym)
|
12
|
+
# >> s2.call(:stub_request)
|
13
|
+
# => :foo
|
14
|
+
def initialize(env = ENV["TENANT"], &getter)
|
15
|
+
@env = (getter || lambda {|k| k}).call(env)
|
16
|
+
end
|
17
|
+
|
18
|
+
def call(_)
|
19
|
+
@env
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Rack::MultiTenant::TenantStrategies
|
2
|
+
class Port
|
3
|
+
# port_map: A hash of ports to tenants
|
4
|
+
#
|
5
|
+
# >> require "ostruct"
|
6
|
+
# >> ports = {3000 => :foo, 3001 => :bar}
|
7
|
+
# >> req = OpenStruct.new(port: 3001)
|
8
|
+
# >> s = Rack::MultiTenant::TenantStrategies::Port.new(ports)
|
9
|
+
# >> s.call(req)
|
10
|
+
# => :bar
|
11
|
+
def initialize(port_map)
|
12
|
+
@port_map = port_map
|
13
|
+
end
|
14
|
+
|
15
|
+
def call(request)
|
16
|
+
tenant = @port_map[request.port]
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module Rack::MultiTenant::TenantStrategies
|
2
|
+
class Subdomain
|
3
|
+
# Gets a tenant by subdomain.
|
4
|
+
#
|
5
|
+
# Optionally takes a block that transforms the tenant string into a
|
6
|
+
# proper tenant.
|
7
|
+
#
|
8
|
+
# >> Sub = Rack::MultiTenant::TenantStrategies::Subdomain
|
9
|
+
# >> require "ostruct"
|
10
|
+
#
|
11
|
+
# >> foo_req = OpenStruct.new(host: "foo.example.com")
|
12
|
+
# >> strat = Sub.new(".example.com")
|
13
|
+
# >> strat.call(foo_req)
|
14
|
+
# => "foo"
|
15
|
+
#
|
16
|
+
# >> strat_with_getter = Sub.new("example.com", &:to_sym)
|
17
|
+
# >> strat_with_getter.call(foo_req)
|
18
|
+
# => :foo
|
19
|
+
#
|
20
|
+
# >> wrong_host_req = OpenStruct.new(host: "localhost")
|
21
|
+
# >> strat.call(wrong_host_req)
|
22
|
+
# => nil
|
23
|
+
def initialize(domain, &getter)
|
24
|
+
@domain, @getter = domain, getter || lambda {|k| k}
|
25
|
+
end
|
26
|
+
|
27
|
+
def call(request)
|
28
|
+
if subdomain = subdomain(request.host, @domain)
|
29
|
+
@getter.call(subdomain)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
def subdomain(host, domain)
|
35
|
+
if loc = host.rindex(domain)
|
36
|
+
host[0, loc].chomp(".")
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'rack-multitenant/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |gem|
|
7
|
+
gem.name = "rack-multitenant"
|
8
|
+
gem.version = Rack::Multitenant::VERSION
|
9
|
+
gem.authors = ["Andrew O'Brien"]
|
10
|
+
gem.email = ["andrew@econify.com"]
|
11
|
+
gem.description = %q{Rack middleware for scoping requests and sharing accounts.}
|
12
|
+
gem.summary = %q{For multitenant applications with shared accounts but different data. Scope requests to the tenant when before it enters the app.}
|
13
|
+
gem.homepage = "https://github.com/Econify/rack-multitenant"
|
14
|
+
|
15
|
+
gem.files = `git ls-files`.split($/)
|
16
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
17
|
+
gem.test_files = gem.files.grep(%r{^(examples|test|spec|features)/})
|
18
|
+
gem.require_paths = ["lib"]
|
19
|
+
|
20
|
+
gem.add_runtime_dependency "rack", "~> 1.4.0"
|
21
|
+
end
|
metadata
ADDED
@@ -0,0 +1,88 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: rack-multitenant
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Andrew O'Brien
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2013-04-22 00:00:00 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: rack
|
16
|
+
prerelease: false
|
17
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 1.4.0
|
22
|
+
type: :runtime
|
23
|
+
version_requirements: *id001
|
24
|
+
description: Rack middleware for scoping requests and sharing accounts.
|
25
|
+
email:
|
26
|
+
- andrew@econify.com
|
27
|
+
executables: []
|
28
|
+
|
29
|
+
extensions: []
|
30
|
+
|
31
|
+
extra_rdoc_files: []
|
32
|
+
|
33
|
+
files:
|
34
|
+
- .gitignore
|
35
|
+
- Gemfile
|
36
|
+
- LICENSE.txt
|
37
|
+
- README.md
|
38
|
+
- Rakefile
|
39
|
+
- examples/example_helper.rb
|
40
|
+
- examples/get_identity.rb
|
41
|
+
- examples/get_identity_spec.rb
|
42
|
+
- examples/getter_strategies.rb
|
43
|
+
- examples/getter_strategies_spec.rb
|
44
|
+
- examples/multitenant_sinatra.rb
|
45
|
+
- lib/rack-multitenant.rb
|
46
|
+
- lib/rack-multitenant/version.rb
|
47
|
+
- lib/rack/multitenant.rb
|
48
|
+
- lib/rack/multitenant/get_current_tenant.rb
|
49
|
+
- lib/rack/multitenant/get_identity.rb
|
50
|
+
- lib/rack/multitenant/tenant_strategies/default.rb
|
51
|
+
- lib/rack/multitenant/tenant_strategies/env_variable.rb
|
52
|
+
- lib/rack/multitenant/tenant_strategies/port.rb
|
53
|
+
- lib/rack/multitenant/tenant_strategies/subdomain.rb
|
54
|
+
- rack-multitenant.gemspec
|
55
|
+
homepage: https://github.com/Econify/rack-multitenant
|
56
|
+
licenses: []
|
57
|
+
|
58
|
+
metadata: {}
|
59
|
+
|
60
|
+
post_install_message:
|
61
|
+
rdoc_options: []
|
62
|
+
|
63
|
+
require_paths:
|
64
|
+
- lib
|
65
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
66
|
+
requirements:
|
67
|
+
- &id002
|
68
|
+
- ">="
|
69
|
+
- !ruby/object:Gem::Version
|
70
|
+
version: "0"
|
71
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- *id002
|
74
|
+
requirements: []
|
75
|
+
|
76
|
+
rubyforge_project:
|
77
|
+
rubygems_version: 2.0.3
|
78
|
+
signing_key:
|
79
|
+
specification_version: 4
|
80
|
+
summary: For multitenant applications with shared accounts but different data. Scope requests to the tenant when before it enters the app.
|
81
|
+
test_files:
|
82
|
+
- examples/example_helper.rb
|
83
|
+
- examples/get_identity.rb
|
84
|
+
- examples/get_identity_spec.rb
|
85
|
+
- examples/getter_strategies.rb
|
86
|
+
- examples/getter_strategies_spec.rb
|
87
|
+
- examples/multitenant_sinatra.rb
|
88
|
+
has_rdoc:
|