Merge branch 'atomaka/feature/authorization' into 'master'

Add authorization via Pundit

Also includes some a refactor to null object pattern for guest users.

See merge request !24
This commit is contained in:
Andrew Tomaka 2015-12-23 20:47:12 +00:00
commit 1f9788309b
31 changed files with 678 additions and 231 deletions

View file

@ -4,4 +4,5 @@ test:
- apt-get install -y nodejs libqtwebkit-dev qt4-qmake sqlite3 libsqlite3-dev
- bundle install --path /cache
- bundle exec rake db:create RAILS_ENV=test
- bundle exec rake db:test:prepare
- bundle exec rspec

10
Gemfile
View file

@ -1,13 +1,5 @@
source 'https://rubygems.org'
def darwin_only(require_as)
RbConfig::CONFIG['host_os'] =~ /darwin/ && require_as
end
def linux_only(require_as)
RbConfig::CONFIG['host_os'] =~ /linux/ && require_as
end
gem 'rails', '4.2.3'
gem 'sqlite3'
@ -25,6 +17,8 @@ gem 'ancestry'
gem 'bcrypt'
gem 'pundit'
gem 'sdoc', '~> 0.4.0', group: :doc
group :development do

View file

@ -212,6 +212,8 @@ GEM
coderay (~> 1.1.0)
method_source (~> 0.8.1)
slop (~> 3.4)
pundit (1.0.1)
activesupport (>= 3.0.0)
quiet_assets (1.1.0)
railties (>= 3.1, < 5.0)
rack (1.6.4)
@ -387,6 +389,7 @@ DEPENDENCIES
launchy
metric_fu
pry
pundit
quiet_assets
rails (= 4.2.3)
rspec-rails
@ -403,4 +406,4 @@ DEPENDENCIES
uglifier (>= 1.3.0)
BUNDLED WITH
1.10.5
1.10.6

View file

@ -6,10 +6,15 @@ class ApplicationController < ActionController::Base
helper_method :current_session
helper_method :logged_in?
include Pundit
rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized
private
def current_user
@current_user ||= User.find(current_session[:user_id]) if current_session
@current_user ||= GuestUser.new
end
def current_session
@ -17,6 +22,11 @@ class ApplicationController < ActionController::Base
end
def logged_in?
!!current_user
current_user.registered?
end
def user_not_authorized
flash[:alert] = 'You are not authorized to perform this action.'
redirect_to(request.referrer || root_path)
end
end

View file

@ -3,22 +3,29 @@ class CommentsController < ApplicationController
before_filter :set_comment, only: [:show, :edit, :update, :destroy]
before_filter :set_post
before_filter :set_subcreddit
after_action :verify_authorized
def show
@comments = @comment
.subtree
.includes(:post, :user)
.arrange(order: :created_at)
authorize @comment
end
def new
@comment = Comment.new(params[:parent_id])
authorize @comment
end
def create
@comment = @post.comments.build comment_params
@comment.user = current_user
authorize @comment
if @comment.save
flash[:notice] = 'Comment saved'
else
@ -29,9 +36,12 @@ class CommentsController < ApplicationController
end
def edit
authorize @comment
end
def update
authorize @comment
if @comment.update comment_params
redirect_to subcreddit_post_path(@subcreddit, @post),
notice: 'Comment updated'
@ -41,6 +51,8 @@ class CommentsController < ApplicationController
end
def destroy
authorize @comment
@comment.destroy
redirect_to subcreddit_post_path(@subcreddit, @post),
notice: 'Comment deleted'

View file

@ -2,23 +2,32 @@
class PostsController < ApplicationController
before_filter :set_post, except: [:index, :new, :create]
before_filter :set_subcreddit
after_action :verify_authorized
def index
@posts = Post.includes(:subcreddit, :user).all
authorize Post
end
def show
@comments = @post.comments.includes(:user).arrange(order: :created_at)
authorize @post
end
def new
@post = Post.new
authorize @post
end
def create
@post = @subcreddit.posts.build(post_params)
@post.user = current_user
authorize @post
if @post.save
redirect_to subcreddit_post_path(@subcreddit, @post),
notice: 'Post created'
@ -28,9 +37,12 @@ class PostsController < ApplicationController
end
def edit
authorize @post
end
def update
authorize @post
if @post.update(post_params)
redirect_to subcreddit_post_path(@subcreddit, @post),
notice: 'Post was updated'
@ -40,6 +52,8 @@ class PostsController < ApplicationController
end
def destroy
authorize @post
@post.destroy
redirect_to subcreddits_path(@subcreddit), notice: 'Post was deleted'

View file

@ -1,23 +1,32 @@
# controllers/subcreddits_controller.rb
class SubcredditsController < ApplicationController
before_filter :set_subcreddit, only: [:show, :edit, :update]
after_action :verify_authorized
def index
@subcreddits = Subcreddit.all
authorize Subcreddit
end
def show
@posts = @subcreddit.posts
authorize @subcreddit
end
def new
@subcreddit = Subcreddit.new
authorize @subcreddit
end
def create
@subcreddit = Subcreddit.new(create_subcreddit_params)
@subcreddit.owner = current_user
authorize @subcreddit
if @subcreddit.save
redirect_to @subcreddit, notice: 'Subcreddit was created!'
else
@ -26,9 +35,12 @@ class SubcredditsController < ApplicationController
end
def edit
authorize @subcreddit
end
def update
authorize @subcreddit
if @subcreddit.update(update_subcreddit_params)
redirect_to @subcreddit, notice: 'Subcreddit was updated!'
else

6
app/models/guest_user.rb Normal file
View file

@ -0,0 +1,6 @@
# models/guest_user.rb
class GuestUser
def registered?
false
end
end

View file

@ -15,6 +15,10 @@ class User < ActiveRecord::Base
validates :username, presence: true, uniqueness: true, sluguuidless: true
validates :password, length: { minimum: 8 }
def registered?
true
end
private
def downcase_email

View file

@ -0,0 +1,37 @@
# policies/application_policy.rb
class ApplicationPolicy
attr_reader :user, :record
def initialize(user, record)
@user = user
@record = record
end
def index?
false
end
def show?
false
end
def create?
false
end
def new?
create?
end
def update?
false
end
def edit?
update?
end
def destroy?
false
end
end

View file

@ -0,0 +1,22 @@
# policies/comment_policy.rb
class CommentPolicy < ApplicationPolicy
def index?
true
end
def show?
true
end
def create?
user.registered?
end
def update?
record.user == user
end
def destroy?
record.user == user
end
end

View file

@ -0,0 +1,22 @@
# policies/post_policy.rb
class PostPolicy < ApplicationPolicy
def index?
true
end
def show?
true
end
def create?
user.registered?
end
def update?
record.user == user
end
def destroy?
record.user == user
end
end

View file

@ -0,0 +1,18 @@
# policies/subcreddit_policy.rb
class SubcredditPolicy < ApplicationPolicy
def index?
true
end
def show?
true
end
def create?
user.registered?
end
def update?
record.owner == user
end
end

View file

@ -5,12 +5,14 @@
button.btn.btn-default type="button"
span.glyphicon.glyphicon-search aria-hidden="true"
- if policy(:post).new?
- if @subcreddit && @subcreddit.id
= link_to 'Submit a new link', new_subcreddit_post_path(@subcreddit), class: 'button btn btn-primary btn-block'
= link_to 'Submit a new text post', new_subcreddit_post_path(@subcreddit), class: 'button btn btn-primary btn-block'
- if policy(:subcreddit).new?
= link_to 'Create your own subcreddit', new_subcreddit_path, class: 'button btn btn-primary btn-block'
- if @subcreddit && @subcreddit.id
- if @subcreddit && @subcreddit.id && policy(@subcreddit).edit?
.title Moderation Tools
.box
ul

View file

@ -3,6 +3,7 @@
.alert.alert-info.in-page
p you are viewing a single comment's thread.
p #{link_to 'view the rest of the comments', subcreddit_post_path(@subcreddit, @post)} →
- if policy(:comment).new?
= "Commenting as: #{current_user.username}"
== render 'comments/form', subcreddit: @subcreddit, post: @post, comment: @post.comments.build, parent: @comment
.comments.contents

View file

@ -4,6 +4,7 @@
.title= "all #{@post.comments_count} comments"
- else
.title= "no comments (yet)"
- if policy(:comment).new?
= "Commenting as: #{current_user.username}"
== render 'comments/form', subcreddit: @subcreddit, post: @post, comment: @post.comments.build, parent: nil
.comments.contents

View file

@ -31,7 +31,7 @@ describe ApplicationController, type: :controller do
context 'when not logged in' do
it 'should return nil' do
expect(controller.send(:current_user)).to be_nil
expect(controller.send(:current_user)).to be_a(GuestUser)
end
end
end

View file

@ -99,7 +99,8 @@ describe CommentsController, type: :controller do
end
describe '#edit' do
let!(:comment) { create(:comment) }
context 'when owner' do
let!(:comment) { create(:comment, user: user) }
before(:each) do
get :edit,
id: comment,
@ -117,12 +118,15 @@ describe CommentsController, type: :controller do
end
end
end
end
context '#update' do
let!(:comment) { create(:comment) }
let(:data) { { content: 'Some edited comment content goes here' } }
context 'with valid data' do
let!(:comment) { create(:comment, user: user) }
context 'when owner' do
before(:each) do
put :update,
id: comment,
@ -166,9 +170,11 @@ describe CommentsController, type: :controller do
end
end
end
end
context '#destroy' do
let!(:comment) { create(:comment) }
context 'when owner' do
let!(:comment) { create(:comment, user: user) }
it 'should delete the post' do
delete :destroy,
@ -186,7 +192,8 @@ describe CommentsController, type: :controller do
post_id: comment.post,
subcreddit_id: comment.post.subcreddit
expect(response).to redirect_to(subcreddit_post_path(assigns(:subcreddit),
expect(response)
.to redirect_to(subcreddit_post_path(assigns(:subcreddit),
assigns(:post)))
end
@ -199,3 +206,4 @@ describe CommentsController, type: :controller do
end
end
end
end

View file

@ -94,7 +94,8 @@ describe PostsController, type: :controller do
end
context '#edit' do
let!(:post) { create(:post) }
context 'when owner' do
let!(:post) { create(:post, user: user) }
before(:each) { get :edit, id: post, subcreddit_id: post.subcreddit }
context 'with valid post' do
@ -107,9 +108,9 @@ describe PostsController, type: :controller do
end
end
end
end
context '#update' do
let!(:post) { create(:post) }
let(:data) do
{
title: 'New title',
@ -117,6 +118,9 @@ describe PostsController, type: :controller do
}
end
context 'when owner' do
let!(:post) { create(:post, user: user) }
context 'with valid data' do
before(:each) do
put :update, id: post, subcreddit_id: post.subcreddit, post: data
@ -154,9 +158,11 @@ describe PostsController, type: :controller do
end
end
end
end
context '#destroy' do
let!(:post) { create(:post, subcreddit: subcreddit) }
context 'when owner' do
let!(:post) { create(:post, user: user, subcreddit: subcreddit) }
it 'should delete the post' do
expect { delete :destroy, subcreddit_id: subcreddit, id: post }
@ -175,3 +181,4 @@ describe PostsController, type: :controller do
end
end
end
end

View file

@ -97,7 +97,8 @@ describe SubcredditsController, type: :controller do
describe '#edit' do
context 'with valid subcreddit' do
let(:subcreddit) { create(:subcreddit) }
context 'when owner' do
let(:subcreddit) { create(:subcreddit, owner: user) }
it 'should assign @subcreddit to the existing subcreddit' do
get :edit, id: subcreddit
@ -112,9 +113,9 @@ describe SubcredditsController, type: :controller do
end
end
end
end
describe '#update' do
let(:subcreddit) { create(:subcreddit) }
let(:data) do
{
closed: '1'
@ -122,6 +123,9 @@ describe SubcredditsController, type: :controller do
end
context 'wth valid data' do
context 'when owner' do
let(:subcreddit) { create(:subcreddit, owner: user) }
it 'should assign @subcreddit to the existing subcreddit' do
put :update, id: subcreddit, subcreddit: data
@ -146,7 +150,6 @@ describe SubcredditsController, type: :controller do
expect(flash[:notice]).to be_present
end
end
context 'with invalid data' do
before(:each) { data[:closed] = 'bad' }
@ -159,3 +162,5 @@ describe SubcredditsController, type: :controller do
end
end
end
end
end

View file

@ -3,15 +3,20 @@ require 'rails_helper'
describe 'Edit Comment', type: :feature do
let!(:user) { create(:user) }
let!(:post) { create(:post) }
let!(:comment) { create(:comment, post: post, user: user) }
context 'when signed in' do
let(:content) { 'Some different data' }
let!(:comment) { create(:comment, post: post, user: user) }
before(:each) { signin(user: user) }
context 'when owner' do
let(:content) { 'Some different data' }
context 'with valid data' do
before(:each) do
visit edit_subcreddit_post_comment_path(post.subcreddit, post, comment)
visit edit_subcreddit_post_comment_path(post.subcreddit,
post,
comment)
fill_in :comment_content, with: content
@ -27,4 +32,25 @@ describe 'Edit Comment', type: :feature do
end
end
end
context 'when not owner' do
let!(:comment) { create(:comment, post: post) }
it 'should not allow editing of comment' do
visit edit_subcreddit_post_comment_path(post.subcreddit, post, comment)
expect(page).to have_content('not authorized')
end
end
end
context 'when not signed in' do
let!(:comment) { create(:comment, post: post) }
it 'should not allow editing of comment' do
visit edit_subcreddit_post_comment_path(post.subcreddit, post, comment)
expect(page).to have_content('not authorized')
end
end
end

View file

@ -1,14 +1,17 @@
require 'rails_helper'
describe 'Edit Post', type: :feature do
let!(:subcreddit) { create(:subcreddit) }
context 'when signed in' do
let!(:user) { create(:user) }
let!(:subcreddit) { create(:subcreddit) }
let!(:post) { create(:post, subcreddit: subcreddit, user: user) }
let(:new_post) { build_stubbed(:post) }
before(:each) { signin(user: user) }
context 'when owner' do
let!(:post) { create(:post, subcreddit: subcreddit, user: user) }
context 'with valid data' do
before(:each) do
visit edit_subcreddit_post_path(subcreddit, post)
@ -30,4 +33,26 @@ describe 'Edit Post', type: :feature do
end
end
end
context 'when not owner' do
let!(:post) { create(:post, subcreddit: subcreddit) }
it 'should notify user that they cannot edit' do
visit edit_subcreddit_post_path(subcreddit, post)
expect(page).to have_content 'not authorized'
end
end
end
context 'when not signed in' do
let!(:user) { GuestUser.new }
let!(:post) { create(:post, subcreddit: subcreddit) }
it 'should notify user they cannot edit' do
visit edit_subcreddit_post_path(subcreddit, post)
expect(page).to have_content 'not authorized'
end
end
end

View file

@ -1,9 +1,10 @@
require 'rails_helper'
describe 'New Post', type: :feature do
let!(:subcreddit) { create(:subcreddit) }
context 'when signed in' do
let!(:user) { create(:user) }
let!(:subcreddit) { create(:subcreddit) }
let!(:post) { build(:post, subcreddit: subcreddit) }
before(:each) { signin(user: user) }
@ -46,4 +47,15 @@ describe 'New Post', type: :feature do
end
end
end
context 'when not signed in' do
let!(:user) { GuestUser.new }
let!(:post) { create(:post, subcreddit: subcreddit) }
it 'should notify user they cannot create' do
visit new_subcreddit_post_path(subcreddit)
expect(page).to have_content 'not authorized'
end
end
end

View file

@ -4,8 +4,13 @@ describe 'Edit Subcreddit', type: :feature do
before(:each) { signout }
context 'when logged in' do
let!(:user) { create(:user) }
before(:each) { signin(user: user) }
context 'when user is owner' do
let!(:subcreddit) { create(:subcreddit, owner: user) }
context 'when board is open' do
let!(:subcreddit) { create(:subcreddit) }
before(:each) do
visit subcreddits_path
click_link 'Edit'
@ -23,7 +28,10 @@ describe 'Edit Subcreddit', type: :feature do
end
context 'when board is closed' do
let!(:subcreddit) { create(:subcreddit, closed_at: Time.now) }
let!(:subcreddit) do
create(:subcreddit, owner: user, closed_at: Time.now)
end
before(:each) do
visit subcreddits_path
click_link 'Edit'
@ -36,4 +44,20 @@ describe 'Edit Subcreddit', type: :feature do
end
end
end
context 'when not owner' do
let!(:subcreddit) { create(:subcreddit) }
it 'should not allow editing of subcreddit' do
visit subcreddits_path
click_link 'Edit'
expect(page).to have_content('not authorized')
end
end
end
context 'when not logged in' do
end
end

View file

@ -4,7 +4,11 @@ describe 'New Subcreddit', type: :feature do
before(:each) { signout }
context 'when not signed in' do
it 'should not be able to create a new subcreddit'
it 'should not be able to create a new subcreddit' do
visit new_subcreddit_path
expect(page).to have_content('not authorized')
end
end
context 'when signed in' do

View file

@ -0,0 +1,15 @@
require 'rails_helper'
describe ApplicationPolicy do
subject { ApplicationPolicy.new(user, object) }
let(:user) { create(:user) }
let(:object) { double('Object') }
it { should_not grant(:index) }
it { should_not grant(:show) }
it { should_not grant(:new) }
it { should_not grant(:create) }
it { should_not grant(:edit) }
it { should_not grant(:update) }
it { should_not grant(:destroy) }
end

View file

@ -0,0 +1,48 @@
require 'rails_helper'
describe CommentPolicy do
subject { CommentPolicy.new(user, comment) }
context 'when user is a guest' do
let(:comment) { create(:comment) }
let(:user) { GuestUser.new }
it { should grant(:index) }
it { should grant(:show) }
it { should_not grant(:new) }
it { should_not grant(:create) }
it { should_not grant(:edit) }
it { should_not grant(:update) }
it { should_not grant(:destroy) }
end
context 'when user is registered' do
let(:user) { create(:user) }
context 'when not owner' do
let(:comment) { create(:comment) }
it { should grant(:index) }
it { should grant(:show) }
it { should grant(:new) }
it { should grant(:create) }
it { should_not grant(:edit) }
it { should_not grant(:update) }
it { should_not grant(:destroy) }
end
context 'when owner' do
let(:comment) { create(:comment, user: user) }
it { should grant(:index) }
it { should grant(:show) }
it { should grant(:new) }
it { should grant(:create) }
it { should grant(:edit) }
it { should grant(:update) }
it { should grant(:destroy) }
end
end
end

View file

@ -0,0 +1,48 @@
require 'rails_helper'
describe PostPolicy do
subject { PostPolicy.new(user, post) }
context 'when user is a guest' do
let(:post) { create(:post) }
let(:user) { GuestUser.new }
it { should grant(:index) }
it { should grant(:show) }
it { should_not grant(:new) }
it { should_not grant(:create) }
it { should_not grant(:edit) }
it { should_not grant(:update) }
it { should_not grant(:destroy) }
end
context 'when user is registered' do
let(:user) { create(:user) }
context 'when not owner' do
let(:post) { create(:post) }
it { should grant(:index) }
it { should grant(:show) }
it { should grant(:new) }
it { should grant(:create) }
it { should_not grant(:edit) }
it { should_not grant(:update) }
it { should_not grant(:destroy) }
end
context 'when owner' do
let(:post) { create(:post, user: user) }
it { should grant(:index) }
it { should grant(:show) }
it { should grant(:new) }
it { should grant(:create) }
it { should grant(:edit) }
it { should grant(:update) }
it { should grant(:destroy) }
end
end
end

View file

@ -0,0 +1,49 @@
require 'rails_helper'
describe SubcredditPolicy do
subject { SubcredditPolicy.new(user, subcreddit) }
context 'when user is a guest' do
let(:subcreddit) { create(:subcreddit) }
let(:user) { GuestUser.new }
it { should grant(:index) }
it { should grant(:show) }
it { should_not grant(:new) }
it { should_not grant(:create) }
it { should_not grant(:edit) }
it { should_not grant(:update) }
it { should_not grant(:destroy) }
end
context 'when user is registered' do
let(:user) { create(:user) }
context 'when not owner' do
let(:subcreddit) { create(:subcreddit) }
it { should grant(:index) }
it { should grant(:show) }
it { should grant(:new) }
it { should grant(:create) }
it { should_not grant(:edit) }
it { should_not grant(:update) }
it { should_not grant(:destroy) }
end
context 'when owner' do
let(:subcreddit) { create(:subcreddit, owner: user) }
it { should grant(:index) }
it { should grant(:show) }
it { should grant(:new) }
it { should grant(:create) }
it { should grant(:edit) }
it { should grant(:update) }
it { should_not grant(:destroy) }
end
end
end

View file

@ -12,11 +12,13 @@ require 'rspec/rails'
require 'shoulda/matchers'
require 'capybara/rails'
require 'capybara/rspec'
require 'pundit/rspec'
require 'support/capybara'
require 'support/database_cleaner'
require 'support/factory_girl'
require 'support/helpers'
require 'support/pundit_matcher'
ActiveRecord::Migration.maintain_test_schema!

View file

@ -0,0 +1,15 @@
RSpec::Matchers.define :grant do |action|
match do |policy|
policy.public_send("#{action}?")
end
failure_message do |policy|
"#{policy.class} does not permit #{action} on #{policy.record} "
+ "for #{policy.user.inspect}."
end
failure_message_when_negated do |policy|
"#{policy.class} does not forbid #{action} on #{policy.record} "
+ "for #{policy.user.inspect}."
end
end