Software Development | Ruby on Rails
altered_beast Authorisation

Whereas Authentication is about validating that the user is who they say they are, authorisation is about who can do what on the site. I had a poke around in altered_beast to see what authorisation capability was built in.

autheniticated_system basics

autheniticated_system contains the base functionality for the authorisation mechanism. It offers a set of functions to use to check the rights of the user.

current_user

The starting point is the current_user function. If there is no current user it will try to retrieve a user using a session, basic HTTP authentication, or a cookie, in that order. Only if all of those fail will it return false.

lib/authenticated_system.rb (snippet)

def current_user
  @current_user ||= (login_from_session || login_from_basic_auth || login_from_cookie) unless @current_user == false
end

logged_in? and login_required

The autheniticated_system provides methods to check whether the user is logged_in?. The check for logged_in? is simply that a current_user exists.

lib/authenticated_system.rb (snippet)

def logged_in?
  !!current_user
end

There are also a related method, login_required, to use as a before_filter. Interestingly this doesn't just check that the user is logged_in? but instead checks that the user is authorized? and denies access if not. More on authorized? later.

lib/authenticated_system.rb (snippet)

def login_required
  authorized? || access_denied
end

admin? and admin_required

The check for admin? is also pretty simple. Is the user logged in and flagged as an admin.

lib/authenticated_system.rb (snippet)

def admin?
  logged_in? && current_user.admin?
end

The check for admin? is scattered through altered_beast including in:

  • app/controllers/posts_controller.rb
  • app/controllers/users_controller.rb
  • app/models/user.rb
  • app/views/campaigns/index.html.erb
  • app/views/forums/index.html.erb
  • app/views/sites/index.html.erb
  • app/views/sites/show.html.erb
  • app/views/topics/show.html.erb
  • app/views/topics/_form.html.erb
  • app/views/users/edit.html.erb
  • app/views/users/index.html.erb
  • app/views/users/show.html.erb

Most of these files are views and the check for admin? is to decide whether or not to display a link to a admin only action.

There is a matching before_filter called admin_required that is used in the Forums, Sites and Users controllers. This checks for admin? and allows/denies access accordingly.

lib/authenticated_system.rb (snippet)

def admin_required
  admin? || access_denied 
end

authorized?

The basic authorized? method just checks that the user is logged in. This, presumably, is why it is used for the before_filter login_required described above.

lib/authenticated_system.rb (snippet)

def authorized?(action = action_name, resource = nil)
  logged_in?
end

You can override the authorized? method in your controllers if you need a more fine tuned authorisation.  altered_beast only overrides the authorized? method in the Sites and Users controllers. For Sites admin_required is the before_filter for all actions so admin rights are guaranteed. Within that context the authorized? method will allow actions if there is no site or if the user is creating a new site.

app/controllers/sites_controller.rb (snippet)

def authorized?
  @site.nil? or @site.new_record? #or current_site == Site.find(:first)
end

Within the Users controller admin rights is not guaranteed for all actions so the first check is for admin?. The check params[:id] == current_user.id.to_s is to allow users to edit their own details. I'm not sure off hand what params[:id].blank? is for; perhaps to allow read actions.

app/controllers/users_controller.rb (snippet)

def authorized?
  admin? || params[:id].blank? || params[:id] == current_user.id.to_s
end

User Roles in altered_beast

altered_beast has a rather ad hoc mechanism for supporting user roles. Admin rights are granted by a flag on the User model. A moderator has a Moderatorship entry. Otherwise it is whether a user is logged_in? or not that is the major factor.

guest i.e. not logged in

Anybody can see the indexes and individual records for:

  • forum
  • topic
  • post

logged_in?

Once logged_in? users can:

  • Create posts and topics
  • Update posts and topics they have previously created
  • Create and delete monitorships

Create and Update Topics 

The Topics controller delegates creation of Topics to the User model.  Notice the @topic = current_user.post @forum, params[:topic] within the create action.

app/controllers/topics_controller.rb (snippet)

def create
  @topic = current_user.post @forum, params[:topic]

  respond_to do |format|
    if @topic.new_record?
      format.html { render :action => "new" }
      format.xml { render :xml => @topic.errors, :status => :unprocessable_entity }
    else
      flash[:notice] = 'Topic was successfully created.'
      format.html { redirect_to(forum_topic_path(@forum, @topic)) }
      format.xml { render :xml => @topic, :status => :created, :location => forum_topic_url(@forum, @topic) }
    end
  end
end

Similarly the update action has current_user.revise @topic, params[:topic] so once again is delegating to the User model although it is the revise method being invoked this time.

app/controllers/topics_controller.rb (snippet)

def update
  current_user.revise @topic, params[:topic]
  respond_to do |format|
    if @topic.errors.empty?
      flash[:notice] = 'Topic was successfully updated.'
      format.html { redirect_to(forum_topic_path(@forum, @topic)) }
      format.xml { head :ok }
    else
      format.html { render :action => "edit" }
      format.xml { render :xml => @topic.errors, :status => :unprocessable_entity }
    end
  end
end

Having a look in the the Posting concern of the User model we a set of related methods:

  • post (create topic)
  • reply (create post)
  • revise (update topic or post)
  • revise_topic (update topic)

Somewhat confusing given the altered_beast data model the post method is to create a Topic rather than a Post. That is because a Topic is the original post and Posts are the replies. The post method ensures the new instance of Topic has the correct parents (Forum and User).

app/models/user/posting.rb (snippet)

def post(forum, attributes)
  attributes.symbolize_keys!
  Topic.new(attributes) do |topic|
    topic.forum = forum
    topic.user = self
    revise_topic topic, attributes, moderator_of?(forum)
  end
end

You can pass in either a Topic or a Post to the revise method, which is why the first parameter is given the generic name (record). The Post and Topic models get the editable_by? method from the module models/user/editable.rb.

app/models/user/posting.rb (snippet)

def revise(record, attributes)
  is_moderator = moderator_of?(record.forum)
  return unless record.editable_by?(self, is_moderator)
  case record
    when Topic then revise_topic(record, attributes, is_moderator)
    when Post then post.save
    else raise "Invalid record to revise: #{record.class.name.inspect}"
  end
  record
end

For Topics there is a little more work to do. revise_topic sets sticky/locked flags if the User a moderator or admin.

app/models/user/posting.rb (snippet)

protected
def revise_topic(topic, attributes, is_moderator)
  topic.title = attributes[:title] if attributes.key?(:title)
  topic.sticky, topic.locked = attributes[:sticky], attributes[:locked] if is_moderator
  topic.save
end

Create and update Posts

Create and edit is handled slightly differently for posts.

app/controllers/posts_controller.rb (snippet)

 before_filter :find_post, :only => [:edit, :update, :destroy]

Users can edit posts they have previously created. Notice the post.user == current_user below:

app/controllers/posts_controller.rb (snippet)

def find_post
  post = @topic.posts.find(params[:id])
  if post.user == current_user || current_user.admin?
    @post = post
  else
    raise ActiveRecord::RecordNotFound
  end
end

 

app/models/user/posting.rb (snippet)

def reply(topic, body)
  returning topic.posts.build(:body => body) do |post|
    post.site = topic.site
    post.forum = topic.forum
    post.user = self
    post.save
  end
end

I hadn't seen Ruby on Rails returning method before. Apparently it is used to clarify the code to generate a return value. The two parameters are the initial value of the return code - in this case topic.posts.build(:body => body) and a block to calculate the final return value.

 

moderator

A User can become a Moderator of a Forum. This role enables the user to update records that they didn't originally create, including:

  • Topics
  • Posts

Strictly speaking in altered_beast a Moderator is a User that has a Moderatorship record linking the User and the Forum. As a classic many-to-many this means a particular Forum can have more than one Moderator and a User might moderate more than one Forum. The relationships are given the User and Forum models.

app/models/forum.rb (snippet)

has_many :moderatorships, :dependent => :delete_all
has_many :moderators, :through => :moderatorships, :source => :user

app/models/user.rb (snippet)

has_many :moderatorships, :dependent => :delete_all
has_many :forums, :through => :moderatorships, :source => :forum do
  def moderatable
    find :all, :select => "#{Forum.table_name}.*, #{Moderatorship.table_name}.id as moderatorship_id"
  end
end

The check for moderator is the moderator_of? method in the User model. This checks whether the user is admin? or whether they are registered as a moderator of a forum by having an appropriate Moderatorship entry.

app/models/user.rb (snippet)

def moderator_of?(forum)
  !!(admin? || Moderatorship.exists?(:user_id => id, :forum_id => forum.id))
end

People with admin rights can create/delete moderatorships on the Show view for a user. This lists the Moderatorships that exist for the User, which can be deleted, and those that are don't exist but which are legal, which can be created.

app/views/users/show.html.erb

<% content_for :right do -%>
<% if admin? %>
<% if @user.active? && !@user.suspended? %>
  <% form_for @user.moderatorships.build do |f| -%>
  <h6><%= I18n.t 'txt.admin.admin_and_moderation', :default => 'Admin &amp; Moderation' %></h6>

  <% unless @user.forums.empty? -%>

  <p><%= I18n.t 'txt.admin.remove_moderated_forum', 
              :default => 'This user can moderate the following forums. Click one to remove.' %></p>

  <ul class="flat">
  <% @user.forums.moderatable.each do |forum| -%>
  <li>
  <%= link_to forum.name, moderatorship_path(forum.moderatorship_id), 
          :method => :delete, :confirm => I18n.t('txt.admin.remove_user_as_moderator', 
          :default => 'Remove user as moderator for {{forum}}?', :forum => forum.name) %>
  </li>
  <% end -%>
  </ul>
  <% end -%>

  <% unless @user.available_forums.empty? -%>
  <p>
  <label><%= I18n.t 'txt.admin.add_as_moderator', :default => 'Add as moderator' %></label><br />
  <%= f.select :forum_id, @user.available_forums.collect { |forum| [forum.name, forum.id] }, :include_blank => " - " %>
  </p>
  <p>
  <%= f.submit I18n.t 'txt.save', :default => 'Save' %>
  <%= f.hidden_field :user_id %>
  </p>
  <% end -%>
  <% end -%>

admin

Admin users have quite extensive rights. They can do everything a a logged_in user or a moderator can do. In addition they can:

  • Create, update, and delete forum
  • Update and delete any posts - even ones they didn't create
  • Create, read, update and delete sites
  • Create and delete Moderatorships, i.e. Moderators

Another quick peek at the User model confirms that people with admin rights automatically get the same capabilities as moderators.

app/models/user.rb (snippet)

def moderator_of?(forum)
  !!(admin? || Moderatorship.exists?(:user_id => id, :forum_id => forum.id))
end

The find_post method in the Posts controller is where admin users get their additional rights to edit, update and destroy posts that they didn't create. Notice the current_user.admin? below:

app/controllers/posts_controller.rb 

def find_post
  post = @topic.posts.find(params[:id])
  if post.user == current_user || current_user.admin?
    @post = post
  else
    raise ActiveRecord::RecordNotFound
  end
end

 

 

monitor