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 & 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
