diff --git a/Gemfile b/Gemfile
index 74d9c493a..5ca7a1a6b 100644
--- a/Gemfile
+++ b/Gemfile
@@ -28,6 +28,7 @@ gem 'foundation-rails'
gem 'acts_as_votable'
gem "recaptcha", require: "recaptcha/rails"
gem 'ckeditor'
+gem 'cancancan'
group :development, :test do
# Call 'byebug' anywhere in the code to stop execution and get a debugger console
diff --git a/Gemfile.lock b/Gemfile.lock
index 14d82a12e..fd9842f39 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -53,6 +53,7 @@ GEM
builder (3.2.2)
byebug (5.0.0)
columnize (= 0.9.0)
+ cancancan (1.12.0)
capistrano (3.4.0)
i18n
rake (>= 10.0.0)
@@ -290,6 +291,7 @@ DEPENDENCIES
acts_as_commentable_with_threading
acts_as_votable
byebug
+ cancancan
capistrano (= 3.4.0)
capistrano-bundler (= 1.1.4)
capistrano-passenger
diff --git a/app/controllers/account_controller.rb b/app/controllers/account_controller.rb
index 9c11691e2..eb0837fee 100644
--- a/app/controllers/account_controller.rb
+++ b/app/controllers/account_controller.rb
@@ -2,6 +2,7 @@ class AccountController < ApplicationController
before_action :authenticate_user!
before_action :set_account
+ load_and_authorize_resource class: "User"
def show
end
diff --git a/app/controllers/admin/base_controller.rb b/app/controllers/admin/base_controller.rb
new file mode 100644
index 000000000..91524b6d3
--- /dev/null
+++ b/app/controllers/admin/base_controller.rb
@@ -0,0 +1,13 @@
+class Admin::BaseController < ApplicationController
+ before_action :authenticate_user!
+
+ skip_authorization_check
+ before_action :verify_administrator
+
+ private
+
+ def verify_administrator
+ raise CanCan::AccessDenied unless current_user.try(:administrator?)
+ end
+
+end
diff --git a/app/controllers/admin/dashboard_controller.rb b/app/controllers/admin/dashboard_controller.rb
new file mode 100644
index 000000000..f7aa5c440
--- /dev/null
+++ b/app/controllers/admin/dashboard_controller.rb
@@ -0,0 +1,6 @@
+class Admin::DashboardController < Admin::BaseController
+
+ def index
+ end
+
+end
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 7551f3f1a..ad69fd511 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -1,6 +1,9 @@
require "application_responder"
class ApplicationController < ActionController::Base
+
+ check_authorization unless: :devise_controller?
+
self.responder = ApplicationResponder
respond_to :html
@@ -11,6 +14,10 @@ class ApplicationController < ActionController::Base
# For APIs, you may want to use :null_session instead.
protect_from_forgery with: :exception
+ rescue_from CanCan::AccessDenied do |exception|
+ redirect_to main_app.root_url, alert: exception.message
+ end
+
private
def set_locale
diff --git a/app/controllers/comments_controller.rb b/app/controllers/comments_controller.rb
index bbad3ca3b..abee2974d 100644
--- a/app/controllers/comments_controller.rb
+++ b/app/controllers/comments_controller.rb
@@ -1,12 +1,12 @@
class CommentsController < ApplicationController
before_action :authenticate_user!
- before_action :set_debate, :set_parent, only: :create
+ before_action :build_comment, only: :create
+ load_and_authorize_resource
respond_to :html, :js
def create
- @comment = Comment.build(@debate, current_user, params[:comment][:body])
@comment.save!
- @comment.move_to_child_of(@parent) if reply?
+ @comment.move_to_child_of(parent) if reply?
Mailer.comment(@comment).deliver_now if email_on_debate_comment?
Mailer.reply(@comment).deliver_now if email_on_comment_reply?
@@ -15,7 +15,6 @@ class CommentsController < ApplicationController
end
def vote
- @comment = Comment.find(params[:id])
@comment.vote_by(voter: current_user, vote: params[:value])
respond_with @comment
end
@@ -25,16 +24,20 @@ class CommentsController < ApplicationController
params.require(:comments).permit(:commentable_type, :commentable_id, :body)
end
- def set_debate
- @debate = Debate.find(params[:debate_id])
+ def build_comment
+ @comment = Comment.build(debate, current_user, params[:comment][:body])
end
- def set_parent
- @parent = Comment.find_parent(params[:comment])
+ def debate
+ @debate ||= Debate.find(params[:debate_id])
+ end
+
+ def parent
+ @parent ||= Comment.find_parent(params[:comment])
end
def reply?
- @parent.class == Comment
+ parent.class == Comment
end
def email_on_debate_comment?
@@ -42,6 +45,6 @@ class CommentsController < ApplicationController
end
def email_on_comment_reply?
- reply? && @parent.author.email_on_comment_reply?
+ reply? && parent.author.email_on_comment_reply?
end
-end
\ No newline at end of file
+end
diff --git a/app/controllers/debates_controller.rb b/app/controllers/debates_controller.rb
index a395f9c5e..d72965432 100644
--- a/app/controllers/debates_controller.rb
+++ b/app/controllers/debates_controller.rb
@@ -1,8 +1,7 @@
class DebatesController < ApplicationController
include RecaptchaHelper
- before_action :set_debate, only: [:show, :edit, :update, :vote]
before_action :authenticate_user!, except: [:index, :show]
- before_action :validate_ownership, only: [:edit, :update]
+ load_and_authorize_resource
def index
if params[:tag]
@@ -56,10 +55,6 @@ class DebatesController < ApplicationController
params.require(:debate).permit(:title, :description, :tag_list, :terms_of_service)
end
- def validate_ownership
- raise ActiveRecord::RecordNotFound unless @debate.editable_by?(current_user)
- end
-
def set_voted_values(debates_ids)
@voted_values = current_user ? current_user.votes_on_debates(debates_ids) : {}
end
diff --git a/app/controllers/moderation/base_controller.rb b/app/controllers/moderation/base_controller.rb
new file mode 100644
index 000000000..0ee155e03
--- /dev/null
+++ b/app/controllers/moderation/base_controller.rb
@@ -0,0 +1,13 @@
+class Moderation::BaseController < ApplicationController
+ before_action :authenticate_user!
+
+ skip_authorization_check
+ before_action :verify_moderator
+
+ private
+
+ def verify_moderator
+ raise CanCan::AccessDenied unless current_user.try(:moderator?) || current_user.try(:administrator?)
+ end
+
+end
diff --git a/app/controllers/moderation/dashboard_controller.rb b/app/controllers/moderation/dashboard_controller.rb
new file mode 100644
index 000000000..ceaddd6f4
--- /dev/null
+++ b/app/controllers/moderation/dashboard_controller.rb
@@ -0,0 +1,6 @@
+class Moderation::DashboardController < Moderation::BaseController
+
+ def index
+ end
+
+end
diff --git a/app/models/ability.rb b/app/models/ability.rb
new file mode 100644
index 000000000..c1fc24ae3
--- /dev/null
+++ b/app/models/ability.rb
@@ -0,0 +1,26 @@
+class Ability
+ include CanCan::Ability
+
+ def initialize(user)
+ # Not logged in users
+ can :read, Debate
+
+ if user # logged-in users
+ can [:read, :update], User, id: user.id
+
+ can [:read, :create, :vote], Debate
+ can :update, Debate do |debate|
+ debate.editable_by?(user)
+ end
+
+ can [:create, :vote], Comment
+
+ if user.moderator? or user.administrator?
+
+ elsif user.administrator?
+
+ end
+ end
+ end
+
+end
diff --git a/app/models/administrator.rb b/app/models/administrator.rb
new file mode 100644
index 000000000..15715a328
--- /dev/null
+++ b/app/models/administrator.rb
@@ -0,0 +1,6 @@
+class Administrator < ActiveRecord::Base
+ belongs_to :user
+ delegate :name, :email, to: :user
+
+ validates :user_id, presence: true, uniqueness: true
+end
diff --git a/app/models/moderator.rb b/app/models/moderator.rb
new file mode 100644
index 000000000..30d4f4394
--- /dev/null
+++ b/app/models/moderator.rb
@@ -0,0 +1,6 @@
+class Moderator < ActiveRecord::Base
+ belongs_to :user
+ delegate :name, :email, to: :user
+
+ validates :user_id, presence: true, uniqueness: true
+end
diff --git a/app/models/user.rb b/app/models/user.rb
index 17573eaa7..4eb832546 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -19,4 +19,12 @@ class User < ActiveRecord::Base
voted = votes.where("votable_type = ? AND votable_id IN (?)", "Debate", debates_ids)
voted.each_with_object({}){ |v,_| _[v.votable_id] = v.vote_flag }
end
+
+ def administrator?
+ @is_administrator ||= Administrator.where(user_id: id).exists?
+ end
+
+ def moderator?
+ @is_moderator ||= Moderator.where(user_id: id).exists?
+ end
end
diff --git a/app/views/admin/dashboard/index.html.erb b/app/views/admin/dashboard/index.html.erb
new file mode 100644
index 000000000..ffc8a281a
--- /dev/null
+++ b/app/views/admin/dashboard/index.html.erb
@@ -0,0 +1 @@
+
<%= t("admin.dashboard.index.title") %>
diff --git a/app/views/moderation/dashboard/index.html.erb b/app/views/moderation/dashboard/index.html.erb
new file mode 100644
index 000000000..16eb4508f
--- /dev/null
+++ b/app/views/moderation/dashboard/index.html.erb
@@ -0,0 +1 @@
+<%= t("moderation.dashboard.index.title") %>
diff --git a/config/i18n-tasks.yml b/config/i18n-tasks.yml
index 0b705daeb..f8cb8bad6 100644
--- a/config/i18n-tasks.yml
+++ b/config/i18n-tasks.yml
@@ -85,6 +85,8 @@ search:
# ignore_missing:
# - 'errors.messages.{accepted,blank,invalid,too_short,too_long}'
# - '{devise,simple_form}.*'
+ignore_missing:
+ - 'unauthorized.*'
## Consider these keys used:
ignore_unused:
@@ -93,6 +95,8 @@ ignore_unused:
# - 'simple_form.{yes,no}'
# - 'simple_form.{placeholders,hints,labels}.*'
# - 'simple_form.{error_notification,required}.:'
+ignore_unused:
+ - 'unauthorized.*'
## Exclude these keys from the `i18n-tasks eq-base' report:
# ignore_eq_base:
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 27c4b4439..3ab672c01 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -12,6 +12,14 @@ en:
create_debate: Create a debate
my_account_link: My account
language: Site language
+ admin:
+ dashboard:
+ index:
+ title: Administration
+ moderation:
+ dashboard:
+ index:
+ title: Moderation
debates:
index:
create_debate: Create a debate
@@ -81,3 +89,7 @@ en:
subject: Someone has commented on your debate
reply:
subject: Someone has replied to your comment
+ unauthorized:
+ default: "You are not authorized to access this page."
+ manage:
+ all: "You are not authorized to %{action} %{subject}."
diff --git a/config/locales/es.yml b/config/locales/es.yml
index 2e2473a1a..541e75189 100644
--- a/config/locales/es.yml
+++ b/config/locales/es.yml
@@ -12,6 +12,14 @@ es:
create_debate: Crea un debate
my_account_link: Mi cuenta
language: Idioma de la página
+ admin:
+ dashboard:
+ index:
+ title: Administración
+ moderation:
+ dashboard:
+ index:
+ title: Moderación
debates:
index:
create_debate: Crea un debate
@@ -81,3 +89,21 @@ es:
subject: Alguien ha comentado en tu debate
reply:
subject: Alguien ha respondido a tu comentario
+ unauthorized:
+ default: "No tienes permiso para acceder a esta página."
+ index:
+ all: "No tienes permiso para listar %{subject}"
+ show:
+ all: "No tienes permiso para ver %{subject}"
+ edit:
+ all: "No tienes permiso para editar %{subject}"
+ update:
+ all: "No tienes permiso para modificar %{subject}"
+ create:
+ all: "No tienes permiso para crear %{subject}"
+ delete:
+ all: "No tienes permiso para borrar %{subject}"
+ manage:
+ all: "No tienes permiso para realizar la acción '%{action}' sobre %{subject}."
+
+
diff --git a/config/routes.rb b/config/routes.rb
index 73a77f60e..74179ba2a 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -17,11 +17,18 @@ Rails.application.routes.draw do
post :vote
end
end
-
end
resource :account, controller: "account", only: [:show, :update]
+ namespace :admin do
+ root to: "dashboard#index"
+ end
+
+ namespace :moderation do
+ root to: "dashboard#index"
+ end
+
# Example of regular route:
# get 'products/:id' => 'catalog#view'
diff --git a/db/migrate/20150807140235_create_administrators.rb b/db/migrate/20150807140235_create_administrators.rb
new file mode 100644
index 000000000..d844531a4
--- /dev/null
+++ b/db/migrate/20150807140235_create_administrators.rb
@@ -0,0 +1,7 @@
+class CreateAdministrators < ActiveRecord::Migration
+ def change
+ create_table :administrators do |t|
+ t.belongs_to :user, index: true, foreign_key: true
+ end
+ end
+end
diff --git a/db/migrate/20150807140346_create_moderators.rb b/db/migrate/20150807140346_create_moderators.rb
new file mode 100644
index 000000000..526a29944
--- /dev/null
+++ b/db/migrate/20150807140346_create_moderators.rb
@@ -0,0 +1,7 @@
+class CreateModerators < ActiveRecord::Migration
+ def change
+ create_table :moderators do |t|
+ t.belongs_to :user, index: true, foreign_key: true
+ end
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index ea3b71925..2d97d8a60 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -11,11 +11,17 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 20150806163142) do
+ActiveRecord::Schema.define(version: 20150807140346) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
+ create_table "administrators", force: :cascade do |t|
+ t.integer "user_id"
+ end
+
+ add_index "administrators", ["user_id"], name: "index_administrators_on_user_id", using: :btree
+
create_table "comments", force: :cascade do |t|
t.integer "commentable_id"
t.string "commentable_type"
@@ -41,6 +47,12 @@ ActiveRecord::Schema.define(version: 20150806163142) do
t.datetime "updated_at", null: false
end
+ create_table "moderators", force: :cascade do |t|
+ t.integer "user_id"
+ end
+
+ add_index "moderators", ["user_id"], name: "index_moderators_on_user_id", using: :btree
+
create_table "taggings", force: :cascade do |t|
t.integer "tag_id"
t.integer "taggable_id"
@@ -105,4 +117,6 @@ ActiveRecord::Schema.define(version: 20150806163142) do
add_index "votes", ["votable_id", "votable_type", "vote_scope"], name: "index_votes_on_votable_id_and_votable_type_and_vote_scope", using: :btree
add_index "votes", ["voter_id", "voter_type", "vote_scope"], name: "index_votes_on_voter_id_and_voter_type_and_vote_scope", using: :btree
+ add_foreign_key "administrators", "users"
+ add_foreign_key "moderators", "users"
end
diff --git a/spec/factories.rb b/spec/factories.rb
index 8e1a2ea6c..fc76685d0 100644
--- a/spec/factories.rb
+++ b/spec/factories.rb
@@ -31,4 +31,12 @@ FactoryGirl.define do
debate
end
-end
\ No newline at end of file
+ factory :administrator do
+ user
+ end
+
+ factory :moderator do
+ user
+ end
+
+end
diff --git a/spec/features/account_spec.rb b/spec/features/account_spec.rb
index 174145a7f..59ee23f5b 100644
--- a/spec/features/account_spec.rb
+++ b/spec/features/account_spec.rb
@@ -10,6 +10,7 @@ feature 'Account' do
login_as(@user)
visit root_path
click_link "My account"
+ expect(current_path).to eq(account_path)
expect(page).to have_selector("input[value='Manuela']")
expect(page).to have_selector("input[value='Colau']")
@@ -48,4 +49,4 @@ feature 'Account' do
expect(page).to have_selector("input[id='account_email_on_debate_comment'][value='1']")
expect(page).to have_selector("input[id='account_email_on_comment_reply'][value='1']")
end
-end
\ No newline at end of file
+end
diff --git a/spec/features/admin_spec.rb b/spec/features/admin_spec.rb
new file mode 100644
index 000000000..a4728623b
--- /dev/null
+++ b/spec/features/admin_spec.rb
@@ -0,0 +1,34 @@
+require 'rails_helper'
+
+feature 'Admin' do
+ let(:user) { create(:user) }
+
+ scenario 'Access as regular user is not authorized' do
+ login_as(user)
+ visit admin_root_path
+
+ expect(current_path).to eq(root_path)
+ expect(page).to have_content "not authorized"
+ end
+
+ scenario 'Access as a moderator is not authorized' do
+ create(:moderator, user: user)
+
+ login_as(user)
+ visit admin_root_path
+
+ expect(current_path).to eq(root_path)
+ expect(page).to have_content "not authorized"
+ end
+
+ scenario 'Access as an administrator is authorized' do
+ create(:administrator, user: user)
+
+ login_as(user)
+ visit admin_root_path
+
+ expect(current_path).to eq(admin_root_path)
+ expect(page).to_not have_content "not authorized"
+ end
+
+end
diff --git a/spec/features/debates_spec.rb b/spec/features/debates_spec.rb
index f8b96e2ed..894b09b19 100644
--- a/spec/features/debates_spec.rb
+++ b/spec/features/debates_spec.rb
@@ -98,9 +98,9 @@ feature 'Debates' do
expect(debate).to be_editable
login_as(create(:user))
- expect {
- visit edit_debate_path(debate)
- }.to raise_error ActiveRecord::RecordNotFound
+ visit edit_debate_path(debate)
+ expect(current_path).to eq(root_path)
+ expect(page).to have_content 'not authorized'
end
scenario 'Update should not be posible if debate is not editable' do
@@ -109,17 +109,19 @@ feature 'Debates' do
expect(debate).to_not be_editable
login_as(debate.author)
- expect {
- visit edit_debate_path(debate)
- }.to raise_error ActiveRecord::RecordNotFound
+ visit edit_debate_path(debate)
+ edit_debate_path(debate)
+ expect(current_path).to eq(root_path)
+ expect(page).to have_content 'not authorized'
end
scenario 'Update should be posible for the author of an editable debate' do
debate = create(:debate)
login_as(debate.author)
- visit debate_path(debate)
- click_link 'Edit'
+ visit edit_debate_path(debate)
+ expect(current_path).to eq(edit_debate_path(debate))
+
fill_in 'debate_title', with: "End child poverty"
fill_in 'debate_description', with: "Let's..."
diff --git a/spec/features/moderation_spec.rb b/spec/features/moderation_spec.rb
new file mode 100644
index 000000000..cd3052f70
--- /dev/null
+++ b/spec/features/moderation_spec.rb
@@ -0,0 +1,34 @@
+require 'rails_helper'
+
+feature 'Admin' do
+ let(:user) { create(:user) }
+
+ scenario 'Access as regular user is not authorized' do
+ login_as(user)
+ visit moderation_root_path
+
+ expect(current_path).to eq(root_path)
+ expect(page).to have_content "not authorized"
+ end
+
+ scenario 'Access as a moderator is authorized' do
+ create(:moderator, user: user)
+
+ login_as(user)
+ visit moderation_root_path
+
+ expect(current_path).to eq(moderation_root_path)
+ expect(page).to_not have_content "not authorized"
+ end
+
+ scenario 'Access as an administrator is authorized' do
+ create(:administrator, user: user)
+
+ login_as(user)
+ visit moderation_root_path
+
+ expect(current_path).to eq(moderation_root_path)
+ expect(page).to_not have_content "not authorized"
+ end
+
+end
diff --git a/spec/models/ability_spec.rb b/spec/models/ability_spec.rb
new file mode 100644
index 000000000..3954ec600
--- /dev/null
+++ b/spec/models/ability_spec.rb
@@ -0,0 +1,67 @@
+require 'rails_helper'
+require 'cancan/matchers'
+
+describe Ability do
+ subject(:ability) { Ability.new(user) }
+ let(:debate) { Debate.new }
+
+ describe "Non-logged in user" do
+ let(:user) { nil }
+
+ it { should be_able_to(:index, Debate) }
+ it { should be_able_to(:show, debate) }
+ it { should_not be_able_to(:edit, Debate) }
+ it { should_not be_able_to(:vote, Debate) }
+ end
+
+ describe "Citizen" do
+ let(:user) { create(:user) }
+
+ it { should be_able_to(:index, Debate) }
+ it { should be_able_to(:show, debate) }
+ it { should be_able_to(:vote, debate) }
+
+ it { should be_able_to(:show, user) }
+ it { should be_able_to(:edit, user) }
+
+ it { should be_able_to(:create, Comment) }
+ it { should be_able_to(:vote, Comment) }
+
+ describe "other users" do
+ let(:other_user) { create(:user) }
+ it { should_not be_able_to(:show, other_user) }
+ it { should_not be_able_to(:edit, other_user) }
+ end
+
+ describe "editing debates" do
+ let(:own_debate) { create(:debate, author: user) }
+ let(:own_debate_non_editable) { create(:debate, author: user) }
+
+ before { allow(own_debate_non_editable).to receive(:editable?).and_return(false) }
+
+ it { should be_able_to(:edit, own_debate) }
+ it { should_not be_able_to(:edit, debate) } # Not his
+ it { should_not be_able_to(:edit, own_debate_non_editable) }
+ end
+ end
+
+ describe "Moderator" do
+ let(:user) { create(:user) }
+ before { create(:moderator, user: user) }
+
+ it { should be_able_to(:index, Debate) }
+ it { should be_able_to(:show, debate) }
+ it { should be_able_to(:vote, debate) }
+
+ end
+
+ describe "Administrator" do
+ let(:user) { create(:user) }
+ before { create(:administrator, user: user) }
+
+ it { should be_able_to(:index, Debate) }
+ it { should be_able_to(:show, debate) }
+ it { should be_able_to(:vote, debate) }
+
+ end
+end
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 93cde956e..4a8fe8fd7 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -7,13 +7,13 @@ describe User do
@user = create(:user)
end
- it "should return {} if no debate" do
+ it "returns {} if no debate" do
expect(@user.votes_on_debates()).to eq({})
expect(@user.votes_on_debates([])).to eq({})
expect(@user.votes_on_debates([nil, nil])).to eq({})
end
- it "should return a hash of debates ids and votes" do
+ it "returns a hash of debates ids and votes" do
debate1 = create(:debate)
debate2 = create(:debate)
debate3 = create(:debate)
@@ -87,4 +87,28 @@ describe User do
end
end
+ describe "administrator?" do
+ it "is false when the user is not an admin" do
+ expect(subject.administrator?).to be false
+ end
+
+ it "is true when the user is an admin" do
+ subject.save
+ create(:administrator, user: subject)
+ expect(subject.administrator?).to be true
+ end
+ end
+
+ describe "moderator?" do
+ it "is false when the user is not a moderator" do
+ expect(subject.moderator?).to be false
+ end
+
+ it "is true when the user is a moderator" do
+ subject.save
+ create(:moderator, user: subject)
+ expect(subject.moderator?).to be true
+ end
+ end
+
end