Skip to content

Commit

Permalink
Add authorization features and a Pundit mixin
Browse files Browse the repository at this point in the history
  • Loading branch information
pedantic-git committed Sep 13, 2017
1 parent 736266d commit 9cf018a
Show file tree
Hide file tree
Showing 12 changed files with 294 additions and 13 deletions.
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ group :test do
gem "shoulda-matchers", "~> 2.8.0", require: false
gem "timecop"
gem "webmock"
gem "pundit"
end

group :staging, :production do
Expand Down
5 changes: 4 additions & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,8 @@ GEM
pry-rails (0.3.6)
pry (>= 0.10.4)
public_suffix (2.0.5)
pundit (1.1.0)
activesupport (>= 3.0.0)
rack (2.0.3)
rack-test (0.6.3)
rack (>= 1.0)
Expand Down Expand Up @@ -268,6 +270,7 @@ DEPENDENCIES
pg
poltergeist
pry-rails
pundit
rack-timeout
rails_stdout_logging
redcarpet
Expand All @@ -280,4 +283,4 @@ DEPENDENCIES
webmock

BUNDLED WITH
1.15.1
1.15.3
26 changes: 24 additions & 2 deletions app/controllers/administrate/application_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,10 @@ def show
end

def new
resource = resource_class.new
authorize_resource(resource)
render locals: {
page: Administrate::Page::Form.new(dashboard, resource_class.new),
page: Administrate::Page::Form.new(dashboard, resource),
}
end

Expand All @@ -40,6 +42,7 @@ def edit

def create
resource = resource_class.new(resource_params)
authorize_resource(resource)

if resource.save
redirect_to(
Expand Down Expand Up @@ -71,7 +74,7 @@ def destroy
flash[:notice] = translate_with_resource("destroy.success")
redirect_to action: :index
end

private

helper_method :nav_link_state
Expand Down Expand Up @@ -104,6 +107,8 @@ def dashboard

def requested_resource
@_requested_resource ||= find_resource(params[:id])
authorize_resource(@_requested_resource)
@_requested_resource
end

def find_resource(param)
Expand Down Expand Up @@ -145,5 +150,22 @@ def show_search_bar?
dashboard.collection_attributes
).any? { |_name, attribute| attribute.searchable? }
end

# Override to conditionally remove action links
def show_action?(action, resource)
true
end
helper_method :show_action?

# How to instantiate a blank instance of this controller's resource
def new_resource
resource_class.new
end
helper_method :new_resource

# Raise an exception if the resource isn't authorized
def authorize_resource(resource)
end

end
end
36 changes: 36 additions & 0 deletions app/controllers/concerns/administrate/punditize.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
module Administrate
module Punditize
extend ActiveSupport::Concern
include Pundit

included do
def scoped_resource
policy_scope_admin super
end

def authorize_resource(resource)
authorize resource
end

def show_action?(action, resource)
Pundit.policy!(pundit_user, resource).send("#{action}?".to_sym)
end

end

private

# Like the policy_scope method in stock Pundit, but allows the 'resolve'
# to be overridden by 'resolve_admin' for a different index scope in Admin
# controllers.
def policy_scope_admin(scope)
ps = Pundit::PolicyFinder.new(scope).scope!.new(pundit_user, scope)
if ps.respond_to? :resolve_admin
ps.resolve_admin
else
ps.resolve
end
end

end
end
16 changes: 9 additions & 7 deletions app/views/administrate/application/_collection.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -63,11 +63,13 @@ to display a collection of resources in an HTML table.
>
<% collection_presenter.attributes_for(resource).each do |attribute| %>
<td class="cell-data cell-data--<%= attribute.html_class %>">
<a href="<%= polymorphic_path([namespace, resource]) -%>"
class="action-show"
>
<%= render_field attribute %>
</a>
<% if show_action? :show, resource -%>
<a href="<%= polymorphic_path([namespace, resource]) -%>"
class="action-show"
>
<%= render_field attribute %>
</a>
<% end -%>
</td>
<% end %>

Expand All @@ -76,7 +78,7 @@ to display a collection of resources in an HTML table.
t("administrate.actions.edit"),
[:edit, namespace, resource],
class: "action-edit",
) %></td>
) if show_action? :edit, resource%></td>
<% end %>

<% if valid_action? :destroy, collection_presenter.resource_name %>
Expand All @@ -86,7 +88,7 @@ to display a collection of resources in an HTML table.
class: "text-color-red",
method: :delete,
data: { confirm: t("administrate.actions.confirm") }
) %></td>
) if show_action? :destroy, resource %></td>
<% end %>
</tr>
<% end %>
Expand Down
2 changes: 1 addition & 1 deletion app/views/administrate/application/edit.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ It displays a header, and renders the `_form` partial to do the heavy lifting.
"#{t("administrate.actions.show")} #{page.page_title}",
[namespace, page.resource],
class: "button",
) if valid_action? :show %>
) if valid_action?(:show) && show_action?(:show, page.resource) %>
</div>
</header>

Expand Down
2 changes: 1 addition & 1 deletion app/views/administrate/application/index.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ It renders the `_table` partial to display details about the resources.
"#{t("administrate.actions.new")} #{page.resource_name.titleize.downcase}",
[:new, namespace, page.resource_path],
class: "button",
) if valid_action? :new %>
) if valid_action?(:new) && show_action?(:new, new_resource) %>
</div>
</header>

Expand Down
2 changes: 1 addition & 1 deletion app/views/administrate/application/show.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ as well as a link to its edit page.
"#{t("administrate.actions.edit")} #{page.page_title}",
[:edit, namespace, page.resource],
class: "button",
) if valid_action? :edit %>
) if valid_action?(:edit) && show_action?(:edit, page.resource) %>
</div>
</header>

Expand Down
69 changes: 69 additions & 0 deletions docs/authorization.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# Authorization

The default configuration of Administrate is "authenticate-only" - once a
user is authenticated, that user has access to every action of every object.

You can add more fine-grained authorization by overriding methods in the
controller.

## Using Pundit

If your app already uses [Pundit](https://github.com/elabs/pundit) for
authorization, you just need to add one line to your
`Admin::ApplicationController`:

```ruby
include Administrate::Punditize
```

This will use all the policies from your main app to determine if the
current user is able to view a given record or perform a given action.

### Further limiting scope

You may want to limit the scope for a given user beyond what they
technically have access to see in the main app. For example, a user may
have all public records in their scope, but you want to only show *their*
records in the admin interface to reduce confusion.

In this case, you can add an additional `resolve_admin` to your policy's
scope and Administrate will use this instead of the `resolve` method.

For example:

```ruby
class PostPolicy < ApplicationPolicy
class Scope < Scope
def resolve
scope.all
end

def resolve_admin
scope.where(owner: user)
end
end
end
```

## Authorization without Pundit

If you use a different authorization library, or you want to roll your own,
you just need to override a few methods in your controllers or
`Admin::ApplicationController`. For example:

```ruby
# Limit the scope of the given resource
def scoped_resource
super.where(user: current_user)
end

# Raise an exception if the user is not permitted to access this resource
def authorize_resource(resource)
raise "Erg!" unless show_action?(params[:action], resource)
end

# Hide links to actions if the user is not allowed to do them
def show_action?(action, resource)
current_user.can? action, resource
end
```
70 changes: 70 additions & 0 deletions spec/controllers/admin/orders_controller_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
require "rails_helper"

# Test Authorization by using the Pundit concern and an example policy,
# which will test all the authorization functionality.

describe Admin::OrdersController, type: :controller do
context "with Punditize concern" do

controller(Admin::OrdersController) do
include Administrate::Punditize
def pundit_user
Customer.first # assume the user is the first Customer
end
end

let!(:user) { create :customer }

before(:each) do
# Create a few orders for the user and a few for other customers
create_list :order, 4, customer: create(:customer)
create_list :order, 7, customer: user
create_list :order, 2, customer: create(:customer)
create_list :order, 2, customer: user
end

# Policies are defined in order_policy.rb
describe 'GET index' do
it 'shows only the records in the admin scope' do
locals = capture_view_locals { get :index }
expect(locals[:resources].count).to eq(9) # only my orders
end
end
describe 'GET new' do
it 'raises a Pundit error' do
expect{get :new}.to raise_error(Pundit::NotAuthorizedError)
end
end
describe 'GET edit' do
it 'allows me to edit records in Arizona' do
az = create :order, customer: user, address_state: 'AZ'
expect{get :edit, id: az.id}.not_to raise_error
end
it 'does not allow me to edit other records' do
ga = create :order, customer: user, address_state: 'GA'
expect{get :edit, id: ga.id}.to raise_error(Pundit::NotAuthorizedError)
end
end
describe 'DELETE destroy' do
it 'never allows me to delete a record' do
o = create :order, customer: user, address_state: 'AZ'
expect{delete :destroy, id: o.id}.to raise_error(Pundit::NotAuthorizedError)
end
end
describe '#show_action?' do
it 'shows edit actions for records in AZ' do
o = create :order, customer: user, address_state: 'AZ'
expect(controller.show_action? :edit, o).to be true
end
it 'does not show edit actions for records elsewhere' do
o = create :order, customer: user, address_state: 'GA'
expect(controller.show_action? :edit, o).to be false
end
it 'never shows destroy actions' do
o = create :order, customer: user, address_state: 'AZ'
expect(controller.show_action? :destroy, o).to be false
end
end

end
end
53 changes: 53 additions & 0 deletions spec/example_app/app/policies/application_policy.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
class ApplicationPolicy
attr_reader :user, :record

def initialize(user, record)
@user = user
@record = record
end

def index?
false
end

def show?
scope.where(:id => record.id).exists?
end

def create?
false
end

def new?
create?
end

def update?
false
end

def edit?
update?
end

def destroy?
false
end

def scope
Pundit.policy_scope!(user, record.class)
end

class Scope
attr_reader :user, :scope

def initialize(user, scope)
@user = user
@scope = scope
end

def resolve
scope
end
end
end
Loading

0 comments on commit 9cf018a

Please sign in to comment.