Re: Restful Authentication with all the bells and whistles (new 9/05/08)

I needed to include the AuthenticatedSystem in my application controller. I tried that initially but still had errors to I removed it thinking it might be something else. Working fine now though. :-)

Re: Restful Authentication with all the bells and whistles (new 9/05/08)

@kschroed: The flashes for the error and notice alerts are probably something you should include in your application wide layout.

@heinvdm: When you run the command to generate the files for the Restful Authentication plugin, it should include the authenticated system file in your application controller automatically. I'll make a note of it in the tutorial.

@austinfromboston: Good catch, I've updated the tutorial with the correction.

Re: Restful Authentication with all the bells and whistles (new 9/05/08)

I have a newbie question. In looking at the code, I don't understand why the Roles class needs the line "has_many :roles, :through => :permissions"?? Shouldn't this rather be "has_many :users, :through => :permissions"?

Thanks,
John

Re: Restful Authentication with all the bells and whistles (new 9/05/08)

Yes, the correct code for role.rb is:

class Role < ActiveRecord::Base  
  has_many :permissions 
  has_many :users, :through => :permissions
end

Re: Restful Authentication with all the bells and whistles (new 9/05/08)

Excellent tutorial.  Thank you!

I have a question though on extending it.  How do you suggest you make a method that only allows users to edit a certain item only if it is theirs?

For example:

posts_controller

...
def edit
  @post = Post.find(params[:id])
  require_to_authorize_as(@post.user) #current_user must be equal to @post.user or else permission_denied.
end
...

Now, obviously (after I've tried it and realized the errors), this doesn't work, because I don't know how to elegantly supersede require_to_authorize_as with check_admin_role.  That means, I don't know how to easily and elegantly still allow users who are admins to edit other users' posts.

Thanks again!

Last edited by ramon.tayag (2008-01-15 11:56:31)

Re: Restful Authentication with all the bells and whistles (new 9/05/08)

The simplest way would be something like:

def edit
  @post = @current_user.posts.find(params[:id])
rescue ActiveRecord::RecordNotFound
  flash[:notice] = 'This post does not belong to you.'
  redirect_to :action => 'index'
end

Don't forget to model the relationship between users and posts and add a column for user_id to the Posts table.

Re: Restful Authentication with all the bells and whistles (new 9/05/08)

Ahh.. I get it now.  Yes, best to limit the scope of the posts you're looking for.  Sorry though, I do have one more: how would you elegantly say, with the way your code is, "rescue it *unless* the user is an admin"?  Maybe something like this?

def edit
  if check_admin_role
    @post = Post.find(params[:id])
  else
    @post = @current_user.posts.find(params[:id])
    rescue ActiveRecord::RecordNotFound
      flash[:notice] = 'This post does not belong to you.'
      redirect_to :action => 'index'
  end
end

Again, sorry to bother you.  I know you wrote the tutorial to give back, not to be the go-to guy for the 100 questions after that smile

Re: Restful Authentication with all the bells and whistles (new 9/05/08)

No, no, not a problem at all, I'm happy to help. And that is an excellent question. 

Try this, which is pretty much along the lines of the code you proposed:

def edit
  if @current_user.has_role?('administrator')
    @post = Post.find(params[:id])
  else
    @post = @current_user.posts.find(params[:id])
  rescue ActiveRecord::RecordNotFound
    flash[:notice] = 'This post does not belong to you.'     
    redirect_to :action => 'index'
end

You might also try adding this code to the user model:

def has_post?(post)
  self.posts.find(post) ? true : false
end

And then the edit action could look like:

def edit
  if @current_user.has_role?('administrator') || @current_user.has_post?(params[:id])
    @post = Post.find(params[:id])
  else
    flash[:notice] = 'You do not have permission to edit that post.'     
    redirect_to :action => 'index'
  end
end

I haven't tested any of this out, so let me know if it works.

Re: Restful Authentication with all the bells and whistles (new 9/05/08)

Thank you!  I merged your suggestions with something else to make the code a bit shorter.  Since I don't like long controllers, I moved some code into the model.

This way, you can have the same rules for authorization for the model throughout the app.  Since in my case, either the user or the admin can edit or destroy a post, I just have one method in the Post model.  If I have different rules if they edit or destroy, I can have more methods like "authorized_to_edit?" and "authorized_to_destroy?".

POST MODEL

def authorized?(user)
  owner == user || user.admin? ##where user.admin? returns if true if the user is admin
end

POSTS_CONTROLLER
def edit
  @post = Post.find params[:id]
  unless @post.authorized?(current_user)
    flash[:notice] = "Sneaky sneaky!"
    redirect_to "where ever"
  end
end

I'm sure this can be optimized and made even nicer but it's working great.  Thanks for your help!  I'm loving the tutorial!

Re: Restful Authentication with all the bells and whistles (new 9/05/08)

I've slightly changed the code for the change_password function to

before_filter :find_user, :only => [:suspend, :unsuspend, :destroy, :purge,  :change_password]
...
def change_password
        return unless request.post?
        if @user.update_attributes(:password=>params[:user][:password], :password_confirmation => params[:user][:password_confirmation])
            @user.save
            flash[:notice]="Password has been changed."
            redirect_to root_path
        else
            render :action => 'change_password'
        end
    end

It works when I go through the web browser but when I run my test against it, It seems to be failing. The test code is below
def test_should_change_password
      login_as :quentin
      post :change_password, :user => { :passsword=>"newpass", :password_confirmation => "newpass"}
      assert flash.has_key?(:notice)
      assert_not_equal users(:quentin).crypted_password, assigns(:user).crypted_password
      assert_redirected_to root_path
     
  end

The error message I get is that it expected the two password to not be matching! I dont understand why my method isnt updating the password for the user?

Will assigns(:user) return the @user object that is within my change_password function?

Re: Restful Authentication with all the bells and whistles (new 9/05/08)

Hello!

Thanks for an excellent tutorial.

I've now successfully set up a new rails 2.0 site using restful authentication and most of the stuff you presented in your tutorial.

As you mentioned, the permission_denied method of authenticated_system.rb needed some improvement. I repeatedly got an exception when using it (can't convert nil to String). The problem had a simple solution. In line 101 of authenticated_system.rb, you will need to replace the environment variable REQUEST_PATH with PATH_INFO.

Re: Restful Authentication with all the bells and whistles (new 9/05/08)

I modified the password_authentication method in sessions_controller.rb to suit my needs.
 

protected
    def password_authentication(login, password)
      self.current_user = User.authenticate(login, password)
      if logged_in?
        successful_login
      else
        failed_login_text = "Your username or password was entered incorrectly, or if this is your first login attempt, you may not have activated your account."
        failed_login_text_disabled = "Your account has been disabled."
        disabled = User.find_by_login_and_enabled(login, 0)
        disabled ? failed_login(failed_login_text_disabled) : failed_login(failed_login_text)
      end
    end

This way users will know whether their account has actually been disabled rather than them assuming that they've entered their information incorrectly or that it's their first time logging in and they haven't activated their account. big_smile

Last edited by fulvio (2008-01-22 08:56:19)

Re: Restful Authentication with all the bells and whistles (new 9/05/08)

ValerieLoy wrote:

With regards to this tutorial, must the version of Rails be 2.0.2 in order for it to work?

I tried this code:
ruby script/generate scaffold Page title:string body:text

and the error is:
wrong constant name Title:stringController

Can someone tell me what's wrong?

If you're not using Rails 2.0 then give this a go in your db/migrate

class CreateRoles < ActiveRecord::Migration
  def self.up
    create_table :roles do |t|
      t.column :rolename, :string
    end
  end

  def self.down
    drop_table :roles
  end
end

Re: Restful Authentication with all the bells and whistles (new 9/05/08)

etandrib wrote:

I keep getting errors about the methods in authenticated_system.rb. Any idea why? I went through the tutorial twice and am getting the same errors. How does rails reference that file in /lib/?

If you're using Rails 1.2.x you should be using the following authenticated_system.rb. The modification is within the access_denied method.

module AuthenticatedSystem
  protected
    # Returns true or false if the user is logged in.
    # Preloads @current_user with the user model if they're logged in.
    def logged_in?
      current_user != :false
    end
   
    # Accesses the current user from the session.
    def current_user
      @current_user ||= (session[:user] && User.find_by_id(session[:user])) || :false
    end
   
    # Store the given user in the session.
    def current_user=(new_user)
      session[:user] = (new_user.nil? || new_user.is_a?(Symbol)) ? nil : new_user.id
      @current_user = new_user
    end
   
    # Check if the user is authorized.
    #
    # Override this method in your controllers if you want to restrict access
    # to only a few actions or if you want to check if the user
    # has the correct rights.
    #
    # Example:
    #
    #  # only allow nonbobs
    #  def authorize?
    #    current_user.login != "bob"
    #  end
    def authorized?
      true
    end

    # Filter method to enforce a login requirement.
    #
    # To require logins for all actions, use this in your controllers:
    #
    #   before_filter :login_required
    #
    # To require logins for specific actions, use this in your controllers:
    #
    #   before_filter :login_required, :only => [ :edit, :update ]
    #
    # To skip this in a subclassed controller:
    #
    #   skip_before_filter :login_required
    #
    def login_required
      username, passwd = get_auth_data
      self.current_user ||= User.authenticate(username, passwd) || :false if username && passwd
      logged_in? && authorized? ? true : access_denied
    end
   
    def not_logged_in_required
      !logged_in? || permission_denied
    end
       
    def check_role(role)
      unless logged_in? && @current_user.has_role?(role)
        if logged_in?
          permission_denied
        else
          access_denied
        end
      end
    end
     
    def check_administrator_role
      check_role('administrator')
    end 
   
    # Redirect as appropriate when an access request fails.
    #
    # The default action is to redirect to the login screen.
    #
    # Override this method in your controllers if you want to have special
    # behavior in case the user is not authorized
    # to access the requested action.  For example, a popup window might
    # simply close itself.
    def access_denied
      respond_to do |accepts|
        accepts.html do
          store_location
          flash[:error] = "You must be logged in to access this feature."
          redirect_to :controller => 'session', :action => 'new'
        end
        accepts.xml do
          headers["Status"]           = "Unauthorized"
          headers["WWW-Authenticate"] = %(Basic realm="Web Password")
          render :text => "Could't authenticate you", :status => '401 Unauthorized'
        end
      end
      false
    end 
   
    def permission_denied
      respond_to do |format|
        format.html do
          store_location
          flash[:error] = "You don't have permission to complete that action."
          domain = "http://localhost:3011" #modify for your application settings
          http_referer = request.env["HTTP_REFERER"]
          request_path = request.env["REQUEST_PATH"]
          full_path = domain + request_path
          if http_referer.nil? || full_path.nil?
            redirect_to root_path
          else
            #Another area that needs to be modified for your app
            #The [0..20] represents the 21 characters in http://localhost:3000
            #You have to set that to the number of characters in your domain name
            if (http_referer[0..20] == domain) && (http_referer != full_path)
              redirect_to http_referer
            else
              redirect_to root_path
            end
          end
        end
        format.xml do
          headers["Status"]           = "Unauthorized"
          headers["WWW-Authenticate"] = %(Basic realm="Web Password")
          render :text => "You don't have permission to complete this action.", :status => '401 Unauthorized'
        end
      end
    end
   
    # Store the URI of the current request in the session.
    #
    # We can return to this location by calling #redirect_back_or_default.
    def store_location
      session[:return_to] = request.request_uri
    end
   
    # Redirect to the URI stored by the most recent store_location call or
    # to the passed default.
    def redirect_back_or_default(default)
      session[:return_to] ? redirect_to_url(session[:return_to]) : redirect_to(default)
      session[:return_to] = nil
    end
   
    # Inclusion hook to make #current_user and #logged_in?
    # available as ActionView helper methods.
    def self.included(base)
      base.send :helper_method, :current_user, :logged_in?
    end

    # When called with before_filter :login_from_cookie will check for an :auth_token
    # cookie and log the user back in if apropriate
    def login_from_cookie
      return unless cookies[:auth_token] && !logged_in?
      user = User.find_by_remember_token(cookies[:auth_token])
      if user && user.remember_token?
        user.remember_me
        self.current_user = user
        cookies[:auth_token] = { :value => self.current_user.remember_token , :expires => self.current_user.remember_token_expires_at }
        flash[:notice] = "Logged in successfully"
      end
    end

  private
    @@http_auth_headers = %w(X-HTTP_AUTHORIZATION HTTP_AUTHORIZATION Authorization)
    # gets BASIC auth info
    def get_auth_data
      auth_key  = @@http_auth_headers.detect { |h| request.env.has_key?(h) }
      auth_data = request.env[auth_key].to_s.split unless auth_key.blank?
      return auth_data && auth_data[0] == 'Basic' ? Base64.decode64(auth_data[1]).split(':')[0..1] : [nil, nil]
    end
end

Re: Restful Authentication with all the bells and whistles (new 9/05/08)

I noticed the destroy method within roles_controller.rb

 

def destroy
    @user = User.find(params[:user_id])
    @role = Role.find(params[:id])
    if @user.has_role?(@role.rolename)
      @user.roles.delete(@role)
    end
    redirect_to :action => 'index'
  end

For some reason it's doing the following when removing a role:

e.g., http://localhost:3000/users/2/roles/2

Logs indicate:

UPDATE permissions SET user_id = NULL WHERE (user_id = 2 AND id IN (2))

It throws this:

Mysql::Error: #22004Column was set to data type implicit default; NULL supplied for NOT NULL column 'user_id' at row 1: UPDATE permissions SET user_id = NULL WHERE (user_id = 2 AND id IN (2))

Shouldn't it be doing something like this instead:

DELETE FROM permissions WHERE (user_id = 2 AND id IN (2))

I wasn't sure what the best way to resolve this was. Any ideas?

Last edited by fulvio (2008-01-22 09:44:41)

Re: Restful Authentication with all the bells and whistles (new 9/05/08)

Hi, I've been googling about a week to solve a ruby-openid problem. And found a working example on Rama McIntosh's blog. http://myutil.com/2007/12/29/openid-2-0 … ails-2-0-2
At first, I thought rails Ticket #10604 patch worked. But it was more complicated than that. Since Rails, ruby-openid, and open_id_authentication plugin versions were not consistent. And Rama McIntosh went through all. You can find Dr. Nick

Last edited by jaigouk (2008-01-25 10:24:01)

Re: Restful Authentication with all the bells and whistles (new 9/05/08)

Honestly,
with the cutting...
and the pasting...
I had might doubts.

Turns out it worked first time.

Thanks!!

Re: Restful Authentication with all the bells and whistles (new 9/05/08)

Question:

I'd I'd like to use the email as the login, would I just have to look for every instance of "login" and replace it with "email"? Hypothetically.

An easy way would be just change the display of "login" so that in the view it shows "e-mail" and use the same validation as email, and auto populate email with login, but if i wanted to do it the right/correct way, how should I got about doing it?

Thanks in advanced!

Re: Restful Authentication with all the bells and whistles (new 9/05/08)

Omarvelous wrote:

Question:

I'd I'd like to use the email as the login, would I just have to look for every instance of "login" and replace it with "email"? Hypothetically.

An easy way would be just change the display of "login" so that in the view it shows "e-mail" and use the same validation as email, and auto populate email with login, but if i wanted to do it the right/correct way, how should I got about doing it?

Thanks in advanced!

If you are saying that you want the a "display name" (which in this case would be email) and then a "login" name which is the normal login like "jstad". But in your views you want it to seem like the user is by email. If this is the case then just do what you listed below changing the view to show email over the login. I personally added a display name field to my implementation.

If you want to use the email as the login name, then just remove the login field or an easier/quicker way would be to set the 'login' field as the users email. Either way there is no 'proper' way to do this, because when you think about it.... if you want the email to be the name people see, that is their login and their email so the current field name fits, just change the validations.

Re: Restful Authentication with all the bells and whistles (new 9/05/08)

jstad wrote:
Omarvelous wrote:

Question:

I'd I'd like to use the email as the login, would I just have to look for every instance of "login" and replace it with "email"? Hypothetically.

An easy way would be just change the display of "login" so that in the view it shows "e-mail" and use the same validation as email, and auto populate email with login, but if i wanted to do it the right/correct way, how should I got about doing it?

Thanks in advanced!

If you are saying that you want the a "display name" (which in this case would be email) and then a "login" name which is the normal login like "jstad". But in your views you want it to seem like the user is by email. If this is the case then just do what you listed below changing the view to show email over the login. I personally added a display name field to my implementation.

If you want to use the email as the login name, then just remove the login field or an easier/quicker way would be to set the 'login' field as the users email. Either way there is no 'proper' way to do this, because when you think about it.... if you want the email to be the name people see, that is their login and their email so the current field name fits, just change the validations.

I realie I should have been more clear...

I'd like to use the E-mail as the form of authentication (username = email). I was saying I could simply make the login validate for email,the same way email does, and perhaps have the e-mail field self populate with whatever the log-in is....

Also maybe incorporating a display name as well...

I guess I'll just do as I said, chenge the view and add the email validation to login.