Administration of a Multi-Tenant Rails App - ruby-on-rails-3

We have a multi-tentant rails app that very closely resembels the "from scratch" approach that Ryan Bates explains/teaches.
Authentication from scratch.
Multi-tenancy from scratch.
We have an account model that has a sub-domain attribute and uses scoping to separate the data.
#ApplicationController
around_filter :scope_current_account
private
def current_account
if request.subdomain.present? && request.subdomain != 'www' && request.subdomain != 'ndt-staging'
#account ||= Account.find_by_subdomain!(request.subdomain)
end
end
helper_method :current_account
def scope_current_account
if request.subdomain.present? && request.subdomain != 'www' && request.subdomain != 'ndt-staging'
Account.current_id = current_account.id
end
yield
ensure
Account.current_id = nil
end
The models:
#Account.rb
has_many :users, :inverse_of => :account, :dependent => :destroy
#User.rb
belongs_to :account, :inverse_of => :users
default_scope { where(account_id: Account.current_id) }
My questino is: What is the best way to manage users application wide.. meaning User.scoped and User.unscoped?
The first thing that comes to mind is to add an admin_password attribute to the User model. Set the password with an environment variable, and at User/Account creation add the admin password value into a hidden field.
(the account new action also builds a user and creates a user account)
#AccountsController
def new
#account = Account.new
#account.users.build(params[:user])
end
The biggest problem I see with this approach is the authentication. I would need to re-write things so that if the admin_password is correct, the normal password attribute will not be checked. If the admin_password is incorrect the password attribute will be used.
As a side note, I've looked at plugins like acts_as_tenant and devise, but would rather build these parts myself.
I could be going down the wrong path here, that is why I am asking for recommended solutions/ideas. Thank you in advance for those :)
#SessionsController
def create
user = User.find_by_email(params[:email].downcase)
if user && user.authenticate(params[:password])
sign_in user
redirect_to welcome_path
else
flash.now[:error] = 'Invalid email/password combination' # Not quite right!
render 'new'
end
end

I solved this using the cancan gem, and creating an admin user in the account create action.

Related

Devise and Facebook Custom Routing

I am trying to create the user registration views and model on my website but I am having a small issue :
I am using devise and omniauth to get the facebook connect features working and it works,
But I want my facebook users when they sign in the first time to create their password,
That is also working, I redirect them to the filled sign up form and they only have to enter their password. But I want them to go to a second "sign_up form" named /views/registrations/new_facebook.html.erb where they can only enter their password and I will also add some other information,
I created the correct view and tested it but I have no idea how to create the correct routes to bypass Devise default
match '/facebook' => 'registrations#new', :as => 'new_facebook_user_registration'
I believe the issue is with match because that's what's not recognised,
If anyone can help me that would be great thanks,
I added my controller code for omniauth :
class OmniauthCallbacksController < Devise::OmniauthCallbacksController
def all
user = User.from_omniauth(request.env["omniauth.auth"])
if user.persisted?
flash[:success] = "Welcome back"
sign_in_and_redirect user
else
session["devise.user_attributes"] = user.attributes
redirect_to new_facebook_user_registration_url
end
end
alias_method :facebook, :all
end
How can I make the redirect_to new_facebook_user_registration_url actually work ?
devise_scope :user do
match "registrations/new_facebook" => "registrations#new_facebook"
end
That's the solution I copied in the registrations controller the new method and named it new_facebook and now everything is working as expected !
I think the issue is that you're not overriding the devise method that redirects to that path. Also according to the devise docs your routes should be set up with a "devise_for" call.
Here's the wiki page describing how to do what you are asking to do, although you may need a bit of custom logic to deal with cases that aren't facebook signups.
https://github.com/plataformatec/devise/wiki/How-To:-Redirect-to-a-specific-page-on-successful-sign-up-(registration)
Some example code from that page:
class RegistrationsController < Devise::RegistrationsController
protected
def after_sign_up_path_for(resource)
'/an/example/path'
end
end
and the one for routes:
devise_for :users, :controllers => { :registrations => "registrations" }

Integrating ActiveAdmin and adauth

I have a running Rails application, using ActiveAdmin and its models to autenticate users. Now I'm interested in moving to an ActiveDirectory authentication, so my users can validate wiht the domain's users.
I've been trying adauth and it looks like a great gem, but I'm a little bit lost when trying to "mix" this gem with my ActiveAdmin authentication. I'm pretty sure I'm not the first one in doing it, so any help would be appreciated.
Thanks!
I finally was able to manage to integrate AD in ActiveAdmin.
Here's what I did, in case someone is interested:
Include gem 'adauth' in your gems
Execute bundle install
Execute rails g adauth:config
Configure the config/initializers/adauth.rb for your AD connection. For example, if your domain is example.com, you must include:
c.domain = "example.com"
c.server = "IP address of your domain controller"
c.base = "dc=example, dc=com"
Execute rails g adauth:sessions
Modify your application_controller.rb. Mine was:
class ApplicationController< ActionController::Base
protect_from_forgery
helper_method :current_user
def current_user
#current_user ||= User.find(session[:user_id]) if session[:user_id]
end
def authenticate_user!
if current_user.nil?
redirect_to '/sessions/new', :error => "Invalid Login"
end
end
end
Execute rails g adauth:user_model user install_adauth.
This creates the migration install_adauth, but for some reason it was empty. I had to fill it myself with:
class InstallAdauth < ActiveRecord::Migration
def up
create_table :users do |u|
u.string 'login'
u.text 'group_strings'
u.string 'name'
u.string 'ou_strings'
end
end
def down
drop_table :users
end
end
Execute rake db:migrate
Modify your sessions_controller.rb. Mine was:
class SessionsController < ApplicationController
def new
redirect_to '/admin' if current_user
end
def create
ldap_user = Adauth.authenticate(params[:username], params[:password])
if ldap_user
user = User.return_and_create_with_adauth(ldap_user)
session[:user_id] = user.id
redirect_to '/admin'
else
redirect_to '/sessions/new', :error => "Invalid Login"
end
end
def destroy
session[:user_id] = nil
redirect_to '/sessions/new'
end
end
So far the validation through ActiveAdmin still works. To switch to ActiveDirectory we must change the file initializers/active_admin.rb
# config.authentication_method = :authenticate_admin_user!
config.authentication_method = :authenticate_user!
#config.current_user_method = :current_admin_user
config.current_user_method = :current_user
In my case, I needed to restart Apache too.
If anytime we want to switch back to ActiveAdmin, we just need to undo the last change

How to configure ActiveAdmin routes to get 2 signin pages (for a user and for an admin) with 1 User model?

My project had 2 models: AdminUser and User.
I'd removed the AdminUser model (and db tables associated) and now I'm using my User model with an admin? method (I've had a boolean admin field in the users table)
ActiveAdmin documentation:
You can skip the Devise user class all together by using the
skip-users flag:
$> rails generate active_admin:install --skip-users
NOTE: If you don’t use the default user settings, you will need to
configure the settings in config/intializers/active_admin.rb to suite
your needs.
Here's what I've found in the initializer:
config.authentication_method = :authenticate_admin_user!
config.current_user_method = :current_admin_user
So, I've modified application_controller.rb as:
def authenticate_admin_user!
render(:file => "#{Rails.root}/public/403.html", :status => 403, :layout => false) and return if user_signed_in? && !current_user.admin?
authenticate_user!
end
def current_admin_user
return nil if user_signed_in? && !current_user.admin?
current_user
end
And in routes.rb:
devise_for :admin_users, ActiveAdmin::Devise.config.merge(:class_name => 'User')
How can I configure ActiveAdmin to access admin section with the ActiveAdmin default path: /admin?
My objective is to have 1 User model, but 2 separate signin pages:
/users/sign_in (default devise signin)
/admin (ActiveAdmin signin)
For now, when I try to access /admin, I'm redirected to /users/sign_in page :-(
Thx for your advices...
Since you skip generating new user model, replace any admin_user string in the generated stuff from ActiveAdmin to user. So instead of :authenticate_admin_user! replace it with :authenticate_user! and so on in migration files: instead of admin_user_id put user_id same for admin_user_type ... etc!

Devise: Create User without Password then Require Password to Use Account?

I'm trying to layout a Rails app using Devise for authentication. I'd like to have an initial landing page where people could enter then email address. I'd like to create an account as soon as I get the email address and then let the user finalize the authentication process later. Is there some documentation that would would show how to do this?
Thanks!
I ended up solving this by using the "devise_invitable" gem. The only thing that I had to extend to get this working was to make sure that a user didn't need to be authenticated to send an invitation. Over-ride the invitations contoller and it's working great:
class InvitationsController < Devise::InvitationsController
include Devise::Controllers::InternalHelpers
skip_filter :authenticate_inviter!
skip_filter :authenticate!
def current_inviter
#current_inviter ||= User.new(params[:user])
end
end
I'm not sure if there is good documentation for how to do this, but it would not be hard. Just don't require authentication on your landing page, or on the post from the login form on that page. Collect an email address in the login form. Send the user a mail to the address they log in with. In the email, include the 'forgot password' link (renamed to 'click here to sign in' ... or whatever) to force the user to login and choose a password. Does that work for you, or did i miss something?
I realize this is late, but this might be helpful to others. I had a similar requirement as you. Basically, I wanted to have a user enter an email address, and persist the user as a guest. Once the user is promoted to a 'regular' user, I re-enable password authentication. First, I created a warden strategy based on the devise-gullible gem. I made a modification to the authenticate method:
class Guest < Authenticatable
def authenticate!
resource = mapping.to.find_for_database_authentication(authentication_hash)
if resource
if resource.respond_to?(:guest?) and resource.guest?
success!(resource)
else
fail(:regular_user)
end
else
fail(:invalid)
end
end
end
I define a guest user in my user model as follows:
def guest?
self.roles.length == 0
end
I'm using CanCan with devise and a HABTM to handle the roles. Currently, I have a 'regular' user and an 'admin' user. A user is a guest if he has no assigned roles yet.
Then, you need to override the devise registrations controller:
class Users::RegistrationsController < Devise::RegistrationsController
prepend_before_filter :allow_params_authentication!, :only => :create
def create
resource = warden.authenticate(:guest,
{ scope: resource_name,
recall: "#{controller_path}#new" })
if resource
set_flash_message(:notice, :signed_in) if is_navigational_format?
sign_in(resource_name, resource)
respond_with resource, :location => after_sign_in_path_for(resource)
elsif warden.winning_strategy.message == :regular_user
set_flash_message :notice, :regular_user if is_navigational_format?
redirect_to new_session_path(resource_name)
else
super
end
end
end
Note that the idea is that we attempt to authenticate a user running the guest strategy only. If he is already registered as a guest, just sign him in normally. If the guest strategy fails because he is registered, but is now a regular user, redirect to the normal sign in page.
Now it is possible to persist some limited information I may wish to collect from a guest, and not require him to have to do a full sign up. I use a normal User model:
class User < ActiveRecord::Base
devise :database_authenticatable, :registerable, :confirmable,
:recoverable, :rememberable, :trackable, :omniauthable,
:token_authenticatable
# Setup accessible (or protected) attributes for your model
attr_accessible :email, :password, :password_confirmation, :remember_me
has_and_belongs_to_many :roles
def self.new_with_session(params, session)
super.tap do |user|
if user.password.blank?
user.password = Devise.friendly_token[0,31]
# Also, we don't need to do email confirmation in this case.
# The user will have the guest role, and we'll do a confirmation
# when we promote him to a 'regular' user.
user.skip_confirmation!
end
end
end
def has_role?(role)
!!self.roles.find_by_name(role.to_s.camelize)
end
end
Note that I auto-generate a password in build_with_session. You can send a auth token at the time of your choosing, and then require the user to set a new password at that time. You'll also want to then change his role so that he becomes a regular user (or do whatever it is you want to note he is no longer a guest).
Here's my partial that appears on the front page:
<%= simple_form_for(resource,
as: resource_name,
url: user_registration_path,
defaults: { required: false },
html: { class: 'well' },
wrapper: :bootstrap,
validate: true) do |f| %>
<%= f.error_notification %>
<fieldset>
<legend>New here? Let's get started!</legend>
<%= f.input :email, placeholder: 'user#example.com', validate: { uniqueness: false } %>
<%= f.button :submit, "Get my quote!", class: 'btn-primary' %>
</fieldset>
<% end %>
So, this form functions as both a guest registration and login form. The only think I don't really care for so far is the the registrations controller is handling an authentication, but I didn't see an immediate way around this.
I implemented this strategy in my local lib directory. Be sure to set your load path appropriately, and add require 'devise_guest' to config/initializers/devise.rb.

How to add a UserProfile to a User when user signs up? (Devise, Rails 3)

I want to override Devise's RegistrationsContollers' create action so that when a user signs up, I can associate a UserProfile model with that user.
So, following the guidelines in the Devise Readme, I override the action:
#File app/controllers/registrations_controller.rb:
class Users::RegistrationsController < Devise::RegistrationsController
def create
# some code here..
self.user_profiles.build #Error (no method `user_profiles`)
current_user.user_profiles.build #Error (current_user is nil)
some other way???
end
end
#File routes.rb:
devise_for :users, :controllers => { :registrations => 'users/registrations' }
Devise is creating a record in the users table, but how do I associate a UserProfile with that record?
I've tried googling but I simply can't get this to work! Any help is much appreciated.
(I'm now using Devise 1.1.5 on Rails 3.0.3)
SOLVED:
Adding solution for benefit of others:
#File app/controllers/registrations_controller.rb:
class Users::RegistrationsController < Devise::RegistrationsController
def create
super
#user.build_user_profile
#user.user_profile.some_data = 'abcd'
#user.save!
end
end
self refers to the contoller not the model in this context.
Also, does the user model have many UserProfiles? Otherwise if they don't (ie they only have one), then you should use #user.build_user_profile, not #user.user_profiles.build
I'd also recommend doing this at the model level, not the controller level, using a callback such as before_create or after_create, ie:
class User < AR
has_one :user_profile
after_create :build_profile
def build_profile
self.build_user_profile
...
end
end