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