#386 Authorization from Scratch Part 2 pro
Oct 11, 2012 | 20 minutes | Security, Authorization
This finishes the series on building authorization from scratch by refactoring the permission logic into a DSL, restricting authorization with attributes, and combining with strong_parameters to protect params.
- Download:
- source codeProject Files in Zip (112 KB)
- mp4Full Size H.264 Video (54.9 MB)
- m4vSmaller H.264 Video (26.2 MB)
- webmFull Size VP8 Video (29.9 MB)
- ogvFull Size Theora Video (64.3 MB)
Resources
application_controller.rb
before_filter :authorize delegate :allow?, to: :current_permission helper_method :allow? delegate :allow_param?, to: :current_permission helper_method :allow_param? private def current_permission @current_permission ||= Permission.new(current_user) end def current_resource nil end def authorize if current_permission.allow?(params[:controller], params[:action], current_resource) current_permission.permit_params! params else redirect_to root_url, alert: "Not authorized." end end
topics_controller.rb
def current_resource @current_resource ||= Topic.find(params[:id]) if params[:id] end
topics/_form.html.erb
<% if allow_param? :topic, :sticky %> ... <% end %>
models/permission.rb
class Permission def initialize(user) allow :users, [:new, :create] allow :sessions, [:new, :create, :destroy] allow :topics, [:index, :show] if user allow :users, [:edit, :update] allow :topics, [:new, :create] allow :topics, [:edit, :update] do |topic| topic.user_id == user.id end allow_param :topic, [:name] allow_all if user.admin? end end def allow?(controller, action, resource = nil) allowed = @allow_all || @allowed_actions[[controller.to_s, action.to_s]] allowed && (allowed == true || resource && allowed.call(resource)) end def allow_all @allow_all = true end def allow(controllers, actions, &block) @allowed_actions ||= {} Array(controllers).each do |controller| Array(actions).each do |action| @allowed_actions[[controller.to_s, action.to_s]] = block || true end end end def allow_param(resources, attributes) @allowed_params ||= {} Array(resources).each do |resource| @allowed_params[resource] ||= [] @allowed_params[resource] += Array(attributes) end end def allow_param?(resource, attribute) if @allow_all true elsif @allowed_params && @allowed_params[resource] @allowed_params[resource].include? attribute end end def permit_params!(params) if @allow_all params.permit! elsif @allowed_params @allowed_params.each do |resource, attributes| if params[resource].respond_to? :permit params[resource] = params[resource].permit(*attributes) end end end end end
spec/models/permission_spec.rb
RSpec::Matchers.define :allow_param do |*args| match do |permission| permission.allow_param?(*args).should be_true end end describe Permission do # ... describe "as admin" do subject { Permission.new(build(:user, admin: true)) } it "allows anything" do should allow(:anything, :here) should allow_param(:anything, :here) end end describe "as member" do let(:user) { create(:user, admin: false) } let(:user_topic) { build(:topic, user: user) } let(:other_topic) { build(:topic) } subject { Permission.new(user) } it "allows topics" do should allow(:topics, :index) should allow(:topics, :show) should allow(:topics, :new) should allow(:topics, :create) should_not allow(:topics, :edit) should_not allow(:topics, :update) should_not allow(:topics, :edit, other_topic) should_not allow(:topics, :update, other_topic) should allow(:topics, :edit, user_topic) should allow(:topics, :update, user_topic) should_not allow(:topics, :destroy) should allow_param(:topic, :name) should_not allow_param(:topic, :sticky) end # ... end end
spec/requests/topics_spec.rb
it "edits own topic as member" do log_in admin: false topic = create(:topic, user: current_user) visit edit_topic_path(topic) page.should_not have_content("Not authorized") end it "cannot create sticky topic as member" do user = create(:user, admin: false, password: "secret") post sessions_path, email: user.email, password: "secret" post topics_path, topic: {name: "Sticky Topic?", sticky: "1"} topic = Topic.last topic.name.should eq("Sticky Topic?") topic.should_not be_sticky end
Also check out the variation with multiple Permission models.