How do I create an admin subdomain to manage subdomains in Rails - ruby-on-rails

I am using AuthLogic and the subdomain method that dhh covered in this blog post, everything is working great, and as expected. What I'm trying to figure out is how to create a subdomain like 'admin' or 'host' that will have a user authenticated from AuthLogic (this may be trivial and unnecessary to mention) that will manage the subdomains. So basically, all subdomains will act normally, except admin.site.com which will go to its own controller and layout..
dhh suggested just throwing in an exception to redirect, but I'm not sure where that goes, it didnt seem that simple to me, any ideas?
EDIT
I think that the fact I am using AuthLogic is important here, because the subdomain logic isnt forwarding users anywhere, once authenticated AuthLogic sends the user to /account - so my question may be related to how do I tell AuthLogic to a different spot if the user is a root user, logging into the admin subdomain..
Here is the code we have implemented thus far
Company Model
class Company < ActiveRecord::Base
has_many :users
has_many :brands, :dependent => :destroy
validates_presence_of :name, :phone, :subdomain
validates_format_of :subdomain, :with => /^[A-Za-z0-9-]+$/, :message => 'The subdomain can only contain alphanumeric characters and dashes.', :allow_blank => true
validates_uniqueness_of :subdomain, :case_sensitive => false
validates_exclusion_of :format, :in => %w( support blog billing help api www host admin manage ryan jeff allie), :message => "Subdomain {{value}} is not allowed."
before_validation :downcase_subdomain
protected
def downcase_subdomain
self.subdomain.downcase! if attribute_present?("subdomain")
end
end
SubdomainCompanies Module
module SubdomainCompanies
def self.included( controller )
controller.helper_method(:company_domain, :company_subdomain, :company_url, :company_account, :default_company_subdomain, :default_company_url)
end
protected
# TODO: need to handle www as well
def default_company_subdomain
''
end
def company_url( company_subdomain = default_company_subdomain, use_ssl = request.ssl? )
http_protocol(use_ssl) + company_host(company_subdomain)
end
def company_host( subdomain )
company_host = ''
company_host << subdomain + '.'
company_host << company_domain
end
def company_domain
company_domain = ''
company_domain << request.domain + request.port_string
end
def company_subdomain
request.subdomains.first || ''
end
def default_company_url( use_ssl = request.ssl? )
http_protocol(use_ssl) + company_domain
end
def current_company
Company.find_by_subdomain(company_subdomain)
end
def http_protocol( use_ssl = request.ssl? )
(use_ssl ? "https://" : "http://")
end
end
Application Controller
class ApplicationController < ActionController::Base
include SubdomainCompanies
rescue_from 'Acl9::AccessDenied', :with => :access_denied
helper :all # include all helpers, all the time
protect_from_forgery # See ActionController::RequestForgeryProtection for details
helper_method :current_user_session, :current_user, :current_company_name
filter_parameter_logging :password, :password_confirmation
before_filter :check_company_status
protected
def public_site?
company_subdomain == default_company_subdomain
end
def current_layout_name
public_site? ? 'public' : 'login'
end
def check_company_status
unless company_subdomain == default_company_subdomain
# TODO: this is where we could check to see if the account is active as well (paid, etc...)
redirect_to default_company_url if current_company.nil?
end
end
end

Look into subdomain-fu which allows you to route to different controllers and actions based on the subdomain. I have done a Railscasts Episode on the subject.
It might looks something like this.
# in routes.rb
map.manage_companies '', :controller => 'companies', :action => 'index', :conditions => { :subdomain => "admin" }
This will need to be high enough up in the routes list so nothing else is matched before it.

FOR RAILS 2.3: You can download a full example app (with a step-by-step tutorial) showing how to implement an admin subdomain, a main domain, and multiple user subdomains using the Devise gem for authentication and the subdomain-routes gem for managing subdomains. Here's the link: subdomain authentication for Rails 2.3.
FOR RAILS 3: Here's a complete example implementation of Rails 3 subdomains with authentication (along with a detailed tutorial). It's much easier to do this in Rails 3 than in Rails 2 (no plugin required).

Related

Ruby on Rails: redirect unauthenticated user to root instead of the sign in page

I am using Rails 4 and Devise 3.
I am using the following in the routes file to prevent access to a page from non authenticated users (not signed in):
authenticate :user do
#page to protect
end
This redirects me to the user/sign_in page, but I want the user to be redirected to the root. So, I added the following as well to the routes page:
get 'user/sign_in' => redirect('/')
But this will mess up what I did in the sessions_controllers:
def new
return render :json => {:success => false, :type => "signinn", :errors => ["You have to confirm your email address before continuing."]}
end
This will stop showing. So, I would like another solution that redirects users to the root directly, instead of having to use authenticate :user and then get 'user/sign_in' => redirect('/').
The following may not have anything to do with redirecting the user to the root, but I would like to explain more about why I am overwriting the new method in the sessions_controller. I moved the sign_in and sign_up views to the root page (home page). In this case, I also needed to hack the error messages so that they appear in the home page, instead of redirecting the user to user/sign_in to show the errors. I used ajax for that.
Update
What I am looking for is something like this:
if user_authenticated?
#show the protected page
else
# redirect the user to the ROOT
end
You can set your devise scope to look something like this.
devise_scope :user do
authenticated :user do
root :to => 'pages#dashboard', as: :authenticated_root
end
unauthenticated :user do
root :to => 'session#new', as: :unauthenticated_root
end
end
EDIT (Based on the new info):
If you have something like this in your routes.rb
root to: 'visitors#index`
then you can have something like this in your visitors_controller.rb
class VisitorsController < ApplicationController
def index
if current_user
redirect_to some_authenticated_path
else
# business logic here
end
end
end
You will still want to handle the propper authorization requirements and authentication requirements in the some_authenticated_path controller and action.
I'm sorry I didn't understand your question, anyway you can do something like this:
class CustomFailure < Devise::FailureApp
def route(scope)
#return super unless [:worker, :employer, :user].include?(scope) #make it specific to a scope
new_user_session_url(:subdomain => 'secure')
end
# You need to override respond to eliminate recall
def respond
if http_auth?
http_auth
else
redirect
end
end
end
And in config/initializers/devise.rb:
config.warden do |manager|
manager.failure_app = CustomFailure
end
This was taken from the wiki of devise:
https://github.com/plataformatec/devise/wiki/How-To%3a-Redirect-to-a-specific-page-when-the-user-can-not-be-authenticated

ActiveAdmin: How to setup HTTP basic authentication?

I want to set basic authentication for ActiveAdmin, which internal devise solution doesn't apply to my case. For that I would like to be able to add middleware to the ActiveAdmin Engine before this is bundled into my app. What I managed to do was:
ActiveAdmin::Engine.configure do |config|
config.middleware.use Rack::Auth::Basic do |username, password|
username == 'admin' && password == 'root'
end
end
But apparently this doesn't make it work, since my active admin routes are still unprotected. How can I effectively do this? And no, I don't want to protect my whole site with basic authentication.
Here's a few ideas:
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
# ...
http_basic_authenticate_with :name => "frodo", :password => "thering", :if => :admin_controller?
def admin_controller?
self.class < ActiveAdmin::BaseController
end
Or, the monkeypatching version
# config/initializers/active_admin.rb
# somewhere outside the setup block
class ActiveAdmin::BaseController
http_basic_authenticate_with :name => "frodo", :password => "thering"
end
If you only want to protect specific resources, you can use the controller block:
# app/admin/users.rb
ActiveAdmin.register Users do
controller do
http_basic_authenticate_with :name => "frodo", :password => "thering"
end
# ...
end
I was hoping that I would be able to extend the controller in this way in config/initializers/active_admin.rb in the setup block, but this didn't work for me:
# app/admin/users.rb
ActiveAdmin.setup do |config|
config.controller do
http_basic_authenticate_with :name => "frodo", :password => "thering"
end
# ...
end
You might try it though, as it could be an ActiveAdmin version thing (I could have sworn that I saw that documented somewhere...)
Good luck, I hope this helps.
UPDATE: A couple more options:
I hadn't realized before that :before_filter in activeadmin config takes a block.
# config/initializers/active_admin.rb
ActiveAdmin.setup do |config|
# ...
config.before_filter do
authenticate_or_request_with_http_basic("Whatever") do |name, password|
name == "frodo" && password == "thering"
end
end
end
And... just one more idea. It sounds like you are not keen on adding anything to application_controller, but this version is not conditional like the first above:
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
def authenticate_admin
authenticate_or_request_with_http_basic("Whatever") do |name, password|
name == "frodo" && password == "thering"
end
end
end
# config/initializers/active_admin.rb
ActiveAdmin.setup do |config|
# ...
config.authentication_method = :authenticate_admin
end
If you just want to protect the admin area of ActiveAdmin, then you should try this:
# app/admin/dashboard.rb
controller do
http_basic_authenticate_with :name => "mega-admin", :password => "supersecret"
end
that works like a charm ;-)
have fun
just another solution for you would be:
# app/controllers/application_controller.rb
protected
def authenticate
authenticate_or_request_with_http_basic do |username, password|
username == "admin" && password == "superpassword"
end
end
# config/initializers/active_admin.rb
config.before_filter :authenticate
the big plus for this solution ist, that you can call
before_filter :authenticate
in every area you want to protet.

HTTP Basic Auth for some (not all) controllers

Using Rails 3.2.
I have half a dozen controllers, and want to protect some (but not all) of them with http_basic_authenticate_with.
I don't want to manually add http_basic_authenticate_with to each controller (I could add another controller in the future and forget to protect it!). It seems the answer is to put it in application_controller.rb with an :except arg which would list the controllers that should not be protected. The problem is, the :except clause wants method names rather than external controller module names, e.g.:
http_basic_authenticate_with :name => 'xxx', :password => 'yyy', :except => :foo, :bar
So then I thought "Wait, since I already have the protected controllers grouped in routes.rb, let's put it there." So I tried this in my routes:
scope "/billing" do
http_basic_authenticate_with :name ...
resources :foo, :bar ...
end
But now I get
undefined method `http_basic_authenticate_with'
What's the best way to approach this?
Do it the way Rails does it.
# rails/actionpack/lib/action_controller/metal/http_authentication.rb
def http_basic_authenticate_with(options = {})
before_action(options.except(:name, :password, :realm)) do
authenticate_or_request_with_http_basic(options[:realm] || "Application") do |name, password|
name == options[:name] && password == options[:password]
end
end
end
All that http_basic_authenticate_with does is add a before_action. You can just as easily do the same yourself:
# application_controller.rb
before_action :http_basic_authenticate
def http_basic_authenticate
authenticate_or_request_with_http_basic do |name, password|
name == 'xxx' && password == 'yyy'
end
end
which means you can use skip_before_action in controllers where this behavior isn't desired:
# unprotected_controller.rb
skip_before_action :http_basic_authenticate

How to add authentication before filter to a rails 3 apps with devise for user sign up and sign in?

Given I'm on Rails 3.1, Ruby 1.9.2 with a standard setup for devise to log a user in by name and password (e.g. similar to https://github.com/RailsApps/rails3-devise-rspec-cucumber ):
I need to add a layer that authenticates a posted username and password against an external service before creating a new user (sign up) or when a user signs in.
(FWIW, further down the road I plan on using the https://github.com/chicks/devise_aes_encryptable strategy/gem to encrypt the sensitive password and, when logging in with a local password, decrypt the one for the remote service, authenticate, then continue with logging in, that is have two passwords, one encrypted one-way, the other reversible... don't ask why, anyway)
In lib/util/authenticate.rb I have an authentication class that returns a boolean against this service e.g.
Util::Authenticate.authenticate(username,password)
But, I can't figure out how to add a filter to authenticate against it on form post before authentication continues (for sign up or sign in).
What I've tried:
I haver a User model and I thought to put a
before_filter :authenticate_against_my_service, :only => [:create, :new]
in the UserController but that didn't work
So, I tried opening up the Devise Sessions controller, which didn't work, nor did subclassing it (e.g. in the README ),
class User::SessionsController < Devise::SessionsController
# something
end
# in config/routes.rb
devise_for :users, :controllers => { :sessions => "users/sessions" }
nor subclassing the Devise Registrations controller (e.g. http://www.tonyamoyal.com/2010/07/28/rails-authentication-with-devise-and-cancan-customizing-devise-controllers/ )and adding a before_filter (same as above).
# in app/controllers/users/registrations_controller.rb
class Users::RegistrationsController < Devise::RegistrationsController
before_filter :check_permissions, :only => [:new, :create, :cancel]
skip_before_filter :require_no_authentication
def check_permissions
authorize! :create, resource
end
end
# and in config/routes.rb
root :to => "home#index"
# replace "devise_for :users" with the below
devise_for :users, :controllers => { :registrations => "users/registrations" }
# other related code
devise_for :users do
get 'logout' => 'devise/sessions#destroy'
end
# resources :users, :only => :show MUST be below devise_for :users
resources :users, :only => :show
I think I have to do this in the controller because once the params get to the model, I won't have an unencrypted password to send to the external service.
I looked at some extensions for ideas such as https://raw.github.com/nbudin/devise_cas_authenticatable/master/lib/devise_cas_authenticatable/strategy.rb e.g.
require 'devise/strategies/base'
module Devise
module Strategies
class CasAuthenticatable < Base
# True if the mapping supports authenticate_with_cas_ticket.
def valid?
mapping.to.respond_to?(:authenticate_with_cas_ticket) && params[:ticket]
end
# Try to authenticate a user using the CAS ticket passed in params.
# If the ticket is valid and the model's authenticate_with_cas_ticket method
# returns a user, then return success. If the ticket is invalid, then either
# fail (if we're just returning from the CAS server, based on the referrer)
# or attempt to redirect to the CAS server's login URL.
def authenticate!
ticket = read_ticket(params)
if ticket
if resource = mapping.to.authenticate_with_cas_ticket(ticket)
# Store the ticket in the session for later usage
if ::Devise.cas_enable_single_sign_out
session['cas_last_valid_ticket'] = ticket.ticket
session['cas_last_valid_ticket_store'] = true
end
success!(resource)
elsif ticket.is_valid?
username = ticket.respond_to?(:user) ? ticket.user : ticket.response.user
redirect!(::Devise.cas_unregistered_url(request.url, mapping), :username => username)
#fail!("The user #{ticket.response.user} is not registered with this site. Please use a different account.")
else
fail!(:invalid)
end
else
fail!(:invalid)
end
end
protected
def read_ticket(params)
#snip
end
end
end
end
Warden::Strategies.add(:cas_authenticatable, Devise::Strategies::CasAuthenticatable)
and read about devise/warden authentication strategies, e.g. https://github.com/hassox/warden/wiki/Strategies but wonder if I need to actually create a new strategy (and if I can figure out how to do that)
EDIT, POSSIBLE SOLUTIONS:
I like alno's suggestion and will try that, though it seems more like a monkeypatch than how devise/warden is meant to be used
module Devise::Models::DatabaseAuthenticatable
alias_method :original_valid_password?, :valid_password?
def valid_password?
if Util::Authenticate.authenticate(username,password)
original_valid_password?
else
false
end
end
end
alternatively, I've looked into adding a warden authentication strategy, but it's hard to figure out all the moving parts, e.g. from Custom authentication strategy for devise
initializers/authentication_strategy.rb: # this could be in initializers/devise.rb as well, no?
Warden::Strategies.add(:custom_external_authentication) do
def valid?
# code here to check whether to try and authenticate using this strategy;
return true # always use the strategy as only user's authenticate
end
def authenticate!
# code here for doing authentication; if successful, call success!
# whatever you've authenticated, e.g. user; if fail, call fail!
if Util::Authenticate.authenticate(username, password)
success!(true) # I don't think I want to return a user, as I'll let database authenticatable handle the rest of the authentication
# I don't think I'm using success! correctly here: https://github.com/hassox/warden/wiki/Strategies
# success!(User.find(someid))
else
fail!("Username and password not valid for external service. Please ensure they are valid and try again.")
end
end
end
add following to initializers/devise.rb
Devise.setup do |config|
config.warden do |manager|
manager.default_strategies.unshift :custom_external_authentication # will this check before or after database authentication? I want before, I think
end
end
OR from How do I add a strategy to Devise
class ExternalServiceStrategy
def valid?
true # always use this
end
def authenticate!
# external boolean service call
end
end
Warden::Strategies.add(:database_authenticatable, ExternalServiceStrategy) # will this work before the db authentication?
If you see in devise source, you will find an valid_password? method, which accepts unencrypted password, so you may override it to authenticate versus some extenal service.
Something like:
def valid_password?(password)
ExternalService.authenticate(email, password)
end
You should be making your changes in the model layer, not in the controller. In fact I would advice you create a file in /lib/whatever that handles talking to the external service, and then modify your User model so that it checks the external service.

Why do you have to explicitly specify scope with friendly_id?

I'm using the friendly_id gem. I also have my routes nested:
# config/routes.rb
map.resources :users do |user|
user.resources :events
end
So I have URLs like /users/nfm/events/birthday-2009.
In my models, I want the event title to be scoped to the username, so that both nfm and mrmagoo can have events birthday-2009 without them being slugged.
# app/models/event.rb
def Event < ActiveRecord::Base
has_friendly_id :title, :use_slug => true, :scope => :user
belongs_to :user
...
end
I'm also using has_friendly_id :username in my User model.
However, in my controller, I'm only pulling out events pertinent to the user who is logged in (current_user):
def EventsController < ApplicationController
def show
#event = current_user.events.find(params[:id])
end
...
end
This doesn't work; I get the error ActiveRecord::RecordNotFound; expected scope but got none.
# This works
#event = current_user.events.find(params[:id], :scope => 'nfm')
# This doesn't work, even though User has_friendly_id, so current_user.to_param _should_ return "nfm"
#event = current_user.events.find(params[:id], :scope => current_user)
# But this does work!
#event = current_user.events.find(params[:id], :scope => current_user.to_param)
SO, why do I need to explicitly specify :scope if I'm restricting it to current_user.events anyway? And why does current_user.to_param need to be called explicitly? Can I override this?
I had the exact same problem with friendly_id 2.2.7 but when I updated to friendly_id 3.0.4 in my Rails 2.3.5 app, everything works. I have test all 4 find invocations you mentioned in my app and they work.
Something to take note of are a few API changes that may affect you. The ones I ran into were:
:strip_diacritics has been replaced with :strip_non_ascii.
I decided to switch to Stringex's String#to_url instead by overriding normalize_friendly_id
resource.has_better_id? is now !resource.friendly_id_status.best?
resource.found_using_numeric_id? is now resource.friendly_id_status.numeric?

Resources