Handcart

A rails gem for managing subdomains in Ruby on Rails.

Installation

RubyGems
gem 'handcart'
Github
gem 'handcart', git: 'https://github.com/mark-d-holmberg/handcart'

Copy Migrations

Handcart requires that you copy over its migrations in the following manner:

rake handcart:install:migrations

Configuration

Handcart can be configured via an initializer file in the following fashion:

# config/initializers/handcart.rb

Handcart.setup do |config|
  # You shouldn't need to change this, but the option is available
  config.subdomain_class = "Handcart::Subdomain"

  # This MUST be set in order for Handcart to work properly.
  # Set it to the value of the class which will call `acts_as_handcart`
  config.handcart_class = "Company"

  # This will allow you to prevent certain subdomains from being created
  config.reserved_subdomains = ["www", "ftp", "ssh", "pop3", "staging", "master"]

  # Set this to provide a custom route to the show action for the model which
  # invokes the call to `acts_as_handcart`.
  config.handcart_show_path = "companies"

  # Set the strategy to use for IP Authorization
  config.ip_authorization_strategy = :inclusion

  # What Rails environments enable IP blocking and blacklisting
  config.global_ip_blocking_enabled_environments = ["development", "production"]

  # What Rails environments enable IP forwarding and forbidden redirects
  config.global_ip_forwarding_enabled_environments = ["development", "staging", "production"]
end

Handcart Activation

If the thing which acts_as_handcart responds to the method active?, Handcart will detect that and will not match the routing for the subdomain constraints for that handcart until that method returns true. This allows your model to require activation before it will actually go live.

Extended Configuration

Handcart has support for default domain constraints using a JSON configuration file. Place the following file inside the config/ folder of your application:

# config/handcart.json

{
  "development" : { "domain_constraint" : "dummy.dev" },
  "test" : { "domain_constraint" : "dummy.test" },
  "staging" : { "domain_constraint" : "dummy.dev" },
  "production" : { "domain_constraint" : "dummy.dev" }
}

You can then use the default constraint provided by this JSON configuration file by doing the following inside your routes

# config/routes.rb

constraints Handcart::DomainConstraint.default_constraint do
  # My Routes here...
end

Advanced Routing Constraints

Handcart provides the ability to configure advanced routing constraints. This will allow you to match routes only if certain conditions are met. See the following example:

  constraints(Handcart::Subdomain) do
    resources :custom_constraints, only: [:index], constraints: Handcart::SettingConstraint.new(:enables?, :custom_constraints)
    match '/', to: 'subdomain#index', via: [:get], as: :subdomain_root
  end

The advanced routing constraint show in this example will only match the route /custom_constraints if the Handcart (Company) can call enables?(:custom_constraints) and returns true. This allows you to check to ensure that routes are match on a per Handcart basis.

Mount the Engine

Handcart needs to be mounted inside the routes file. Add the following line where appropriate:

mount Handcart::Engine => "/handcart"

Setup Handcart Model

Handcart needs to know what model is the thing that is being subdomained, i.e. a School or a Company.

# app/models/company.rb

class Company < ActiveRecord::Base
  acts_as_handcart
end

Provided Helpers

Handcart gives you a few helper methods which can be used in the controller or views

  • current_subdomain

This helper will allow you to fetch the current subdomain (assuming you're on one).

  • current_handcart

This helper will allow you to fetch the current thing that is being subdomained. In the case of the company model above, current_handcart would return the company.

  • handcart_show_path(handcart)

This is a special route generated by Handcart which will allow the backend to link to the show page for the thing which acts_as_handcart.

If in the configuration for Handcart the handcart_show_path was set to "companies", it would result in the following URL:

/companies/:id

Where ID would be set to the id of the company. If this configuration option is not set, it will try to infer it based on the handcart_class that is specified.

Routing Helpers

Handcart provides a way to apply domain/subdomain constraints to your routes. The following is an example which uses both Domain Constraints and Subdomain Constraints.

# config/routes.rb

Rails.application.routes.draw do
  # Allows for a Reserved Subdomain (Master Backend Interface)
  constraints(subdomain: /master/) do
    scope module: 'master' do
      match '/', to: 'dashboard#index', via: [:get], as: :master_root
    end
  end

  # Allows for Subdomains created through Handcart (Franchisee Interface)
  constraints(Handcart::Subdomain) do
    match '/', to: 'subdomain#index', via: [:get], as: :subdomain_root
  end

  # Allows for Domain Constraints (Public Interface)
  constraints Handcart::DomainConstraint.default_constraint do
    match '/', to: 'public#index', via: [:get], as: :public_root
  end
end

IP Authorization

Handcart provides support for foreign IP authorization using various built-in strategies. The IP authorization module provides the following strategies for use when trying to authenticate foreign IP addresses:

None

If the config.ip_authorization_strategy configuration variable is set to :none, then no foreign IP authorization will occur. This means that any IP address from the outside world will be permitted to login to the franchisee interface.

Containment

Setting config.ip_authorization_strategy to :containment will require that the foreign IP address is simple included in the list of IP addresses which are allowed for the franchisee.

Inclusion

Setting config.ip_authorization_strategy to :inclusion will require that the foreign IP address is included in the list of IP addresses which are allowed for the franchisee AND that the subnet of the foreign IP address is also in the same subnet as any which are in the whitelist.

IP Forwarding

If your application has a public interface, Handcart can be configured to automatically forward authorized foreign IP addresses to a controller action of your choosing. In addition, if they are not authorized, it will forward them to a public controller action of your choosing. To invoke IP forwarding the following settings must be configured in the initializer:

  # config/initializers/handcart.rb
  # What Rails environments enable IP forwarding and forbidden redirects
  config.global_ip_forwarding_enabled_environments = ["development", "staging", "production"]

Make sure that the environments in which you want IP forwarding to occur in are listed in the variable above.

Public Controller Invocation

To have Handcart enable IP forwarding, the following method must be invoked in the PublicController:

  # app/controllers/public_controller.rb
  enable_forwarding("subdomain#index", "public#forbidden", only: [:index])

The first argument indicates what controller and action should be invoked inside the franchisee namespace. The second argument is the controller and action on the PublicController which should be invoked when the foreign IP is NOT authorized.

IP Blocking

If you want Handcart to enable the IP blocking feature, you'll need to have the following settings in the initializer:

  # What Rails environments enable IP blocking and blacklisting
  config.global_ip_blocking_enabled_environments = ["development", "production"]

Franchisee Controller Invocation

To have Handcart enable IP blocking, the following method must be invoked in a controller which is namespaced under the franchisee:

  # app/controllers/sessions_controller.rb
  enable_blocking("public#blocked")

The first argument indicates the controller and action which will be redirected to should the IP authorization strategy indicate that the foreign IP address is NOT authorized. This method can also accept the standard options such as only: [:index]. The use case for this method is designed to be the sessions controller for the franchisee. The idea is that it will not let them login to the franchise if they're not authorized.