I have action caching working on my Sites index, and set up a SiteSweeper that works fine:
# app/controllers/admin/sites_controller.rb
class Admin::SitesController < Admin::BaseController
cache_sweeper :site_sweeper, :only => [:create, :update, :destroy]
caches_action :index, :cache_path => '/admin/sites'
...
# app/sweepers/site_sweeper.rb
class SiteSweeper < ActionController::Caching::Sweeper
observe Site
def after_save(site)
expire_cache(site)
end
def after_destroy(site)
expire_cache(site)
end
def expire_cache(site)
expire_action '/admin/sites'
end
end
But I also want to expire /admin/sites whenever any Publishers are saved or destroyed. Is it possible to have a PublisherSweeper expire the Sites index with something like this?
# app/sweepers/publisher_sweeper.rb
class PublisherSweeper < ActionController::Caching::Sweeper
observe Publisher
def after_save(publisher)
expire_cache(publisher)
end
def after_destroy(publisher)
expire_cache(publisher)
end
def expire_cache(publisher)
expire_action '/admin/sites'
end
end
I know I can just call expire_action '/admin/sites' within the various Publisher actions. I'm just wondering if sweepers have this capability (to keep my controllers a bit cleaner).
One sweeper can observe many Models, and any controller can have multiple sweepers.
I think you should change your logic to use something like that:
class SiteSweeper < ActionController::Caching::Sweeper
observe Site, Publisher
(…)
end
On PublishersController
cache_sweeper :site_sweeper, :admin_sweeper
So you don't repeat the logic of cleaning the /admin/site. Call it AdminSweeper, so when something goes wrong you know the only one place that expired the "/admin/sites" action.
Related
Everyone is like this is sweeper. But can anyone please tell me how to create sweeper file for a particular controller. I copy pasted the code but its not working.
Sweepers are the terminators of the caching world and responsible for expiring caches when model objects change. They do this by being half-observers, half-filters and implementing callbacks for both roles. A Sweeper example:
class ImageSweeper < ActionController::Caching::Sweeper
observe Image
def after_save(record)
def after_save(image)
expire_cache(image)
end
def after_destroy(image)
expire_cache(image)
end
def expire_cache(image)
##expire_fragment #'image'
expire_cache(image)
end
end
The sweeper is assigned in the controllers that wish to have its job performed using the cache_sweeper class method:
class ImageController < ApplicationController
cache_sweeper :image_sweeper, :only => [ :edit, :destroy, :share ]
end
In the example above, three actions are responsible for expiring those caches
Above will only work if you have config.action_controller.perform_caching = true in development.rb
I got it working. Sweeper file will be in app/sweepers/controllername(without s)_sweeper.rb
I am aware that several gems are made to handle authorization in Rails. But is it really worth it to use these gems for simple access controls ?
I only have a few "roles" in my application, and I feel that a powerful gem would be useless and even slow down the response time.
I have already implemented a solution, but then I took some security classes (:p) and I realized my model was wrong ("Allow by default, then restrict" instead of "Deny by default, then allow").
Now how can I simply implement a "deny by default, allow on specific cases" ?
Basically I'd like to put at the very top of my ApplicationController
class ApplicationController < ApplicationController::Base
before_filter :deny_access
And at the very top of my other controllers :
class some_controller < ApplicationController
before_filter :allow_access_to_[entity/user]
These allow_access_to_ before_filters should do something like skip_before_filter
def allow_access_to_[...]
skip_before_filter(:deny_access) if condition
end
But this doesn't work, because these allow_access before filters are not evaluated before the deny_access before_filter
Any workaround, better solution for this custom implementation of access control ?
EDIT
Many non-RESTful actions
I need per-action access control
undefined method 'skip_before_filter' for #<MyController... why ?
My before_filters can get tricky
before_action :find_project, except: [:index, :new, :create]
before_action(except: [:show, :index, :new, :create]) do |c|
c.restrict_access_to_manager(#project.manager)
end
I would really advise using a proper battle tested gem for authentication & authorisation instead of rolling your own. These gems have enormous test suites and aren't really all that hard to setup.
I've recently implemented an action based authorization using roles with Pundit & Devise
Devise is changeable as long as the gem you are using provides a current_user method if you don't want to further configure pundit.
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
include Pundit
rescue_from Pundit::NotAuthorizedError, with: :rescue_unauthorized
# Lock actions untill authorization is performed
before_action :authorize_user
# Fallback when not authorized
def rescue_unauthorized(exception)
policy_name = exception.policy.class.to_s.underscore
flash[:notice] = t(
"#{policy_name}.#{exception.query}",
scope: "pundit",
default: :default
)
redirect_to(request.referrer || root_path)
end
end
# app/models/user.rb
class User < ActiveRecord::Base
has_many :roles, through: :memberships
def authorized?(action)
claim = String(action)
roles.pluck(:claim).any? { |role_claim| role_claim == claim }
end
end
# app/policies/user_policy.rb => maps to user_controller#actions
class UserPolicy < ApplicationPolicy
class Scope < Scope
attr_reader :user, :scope
# user is automagically set to current_user
def initialize(user, scope)
#user = user
#scope = scope
end
def resolve
scope.all
end
end
def index?
# If user has a role which has the claim :view_users
# Allow this user to use the user#index action
#user.authorized? :view_users
end
def new?
#user.authorized? :new_users
end
def edit?
#user.authorized? :edit_users
end
def create?
new?
end
def update?
edit?
end
def destroy?
#user.authorized? :destroy_users
end
end
Long story short:
If you configure pundit to force authorization on each request which is described in detail on the github page, the controller evaluates a policy based on the used controller.
UserController -> UserPolicy
Actions get defined with a question mark, even non restful routes.
def index?
# authorization is done inside the method.
# true = authorization succes
# false = authorization failure
end
This is my solution to action based authorization hope it helps you out.
Optimisations & feedback are welcome !
Rolling your own implementation isn't necessarily bad as long as you're committed to it.
It won't get tested and maintained by the community so you must be willing to maintain it yourself in the long run, and if it compromises security you need to be really sure of what you're doing and take extra care. If you have that covered and the existing alternatives don't really fit your needs, making your own isn't such a bad idea. And generally it's an incredibly good learning experience.
I rolled my own with ActionAccess and I couldn't be happier with the results.
Locked by default aproach:
class ApplicationController < ActionController::Base
lock_access
# ...
end
Per-action access control:
class ArticlesController < ApplicationController
let :admins, :all
let :editors, [:index, :show, :edit, :update]
let :all, [:index, :show]
def index
# ...
end
# ...
end
Really lighweight implementation.
I encourage you not to use it but to check out the source code, it has a fare share of comments and should be a good source of inspiration. ControllerAdditions might be a good place to start.
ActionAccess follows a different approach internally, but you can refactor your answer to mimic it's API with something like this:
module AccessControl
extend ActiveSupport::Concern
included do
before_filter :lock_access
end
module ClassMethods
def lock_access
unless #authorized
# Redirect user...
end
end
def allow_manager_to(actions = [])
prepend_before_action only: actions do
#authorized = true if current_user_is_a_manager?
end
end
end
end
class ApplicationController < ActionController::Base
include AccessControl # Locked by default
# ...
end
class ProjectController < ApplicationController
allow_managers_to [:edit, :update] # Per-action access control
# ...
end
Take this example as pseudo-code, I haven't tested it.
Hope this helps.
I didn't like my previous solution using prepend_before_action, here is a nice implementation using ActionController callbacks
module AccessControl
extend ActiveSupport::Concern
class UnauthorizedException < Exception
end
class_methods do
define_method :access_control do |*names, &blk|
_insert_callbacks(names, blk) do |name, options|
set_callback(:access_control, :before, name, options)
end
end
end
included do
define_callbacks :access_control
before_action :deny_by_default
around_action :perform_if_access_granted
def perform_if_access_granted
run_callbacks :access_control do
if #access_denied and not #access_authorized
#request_authentication = true unless user_signed_in?
render(
file: File.join(Rails.root, 'app/views/errors/403.html'),
status: 403,
layout: 'error')
else
yield
end
end
end
def deny_by_default
#access_denied ||= true
end
def allow_access
#access_authorized = true
end
end
end
Then you can add your own allow_access_to_x methods (for example in the same AccessControl concern) :
def allow_access_to_participants_of(project)
return unless user_signed_in?
allow_access if current_user.in?(project.executants)
end
Use it in your controllers the following way (don't forget to include AccessControl in your ApplicationController
class ProjectsController < ApplicationController
access_control(only: [:show, :edit, :update]) do
set_project
allow_access_to_participants_of(#project)
allow_access_to_project_managers
end
def index; ...; end;
def show; ...; end;
def edit; ...; end;
def update; ...; end;
def set_project
#project = Project.find(params[:project_id])
end
end
EDIT : Outdated answer, I have a friendlier implementation that involves using an access_control block
Going with evanbikes suggestion, for now I'll be using prepend_before action. I find it quite simple & flexible, but if I ever realize it's not good enough I will try other things.
Also if you find security issues/other problems with the solution below, please comment and/or downvote. I don't like leaving bad examples in SO.
class ApplicationController < ApplicationController::Base
include AccessControl
before_filter :access_denied
...
My Access Control module
module AccessControl
extend ActiveSupport::Concern
included do
def access_denied(message: nil)
unless #authorized
flash.alert = 'Unauthorized access'
flash.info = "Authorized entities : #{#authorized_entities.join(', '}" if #authorized_entities
render 'static_pages/home', :status => :unauthorized
end
end
def allow_access_to_managers
(#authorized_entities ||= []) << "Project managers"
#authorized = true if manager_logged_in?
end
...
How I use the AC in controllers :
class ProjectController < ApplicationController
# In reverse because `prepend_` is LIFO
prepend_before_action(except: [:show, :index, :new, :create]) do |c|
c.allow_access_to_manager(#manager.administrateur)
end
prepend_before_action :find_manager, except: [:index, :new, :create]
I'm a Rails beginner and I learn that I always must try to be more DRY.
I'm have a comment system associated to my content model, and I load my comment with ajax on page scroll.
In my view I have:
%section.article-comments{'data-url' => content_comments_path(#content)}
and in my routes.rb file I have the route
resources :contents, only: :index do
resources :comments, only: :index
end
My comment controller of course is
def index
#content = Content.find(params[:content_id])
#comments = #content.comments
render ...
end
Now I want to add comments also to videos and gallery.
So I need to add a route for every resource and I need a gallery_index and a video_index.
Content, video and gallery index method in comment controlelr are repeated, and I cannot understand how can I be more DRY.
All your controllers presumably inherit from ApplicationController:
class CommentsController < ApplicationController
If you find yourself with a lot of repetition in any of the controller methods you could define it in ApplicationController instead, with maybe some specific processing in each controller.
For example:
class ApplicationController < ActionController::Base
def index
...some common processing...
specific_index_processing
end
private
def specific_index_processing
# empty method; will be overridden by each controller as required
end
end
class CommentsController < ApplicationController
private
def specific_index_processing
...specific procesing for the comments index method...
end
end
And of course, if one of your controllers needs to be completely different from this common approach you can always just override the entire index method.
Is there a convention in Rails to put Sweeper classes in a particular directory location?
UPDATE: Since observers are put into app/models, I'm assuming sweepers are no different, as long as the name always ends with "sweeper".
I like to put them in the app/sweepers directory.
I also put Presenters in the app/presenters directory...and Observers in the app/observers directory.
Try putting them in the app/models directory.
Sweepers
Cache sweeping is a mechanism which allows you to get around having a ton of expire_{page,action,fragment} calls in your code. It does this by moving all the work required to expire cached content into
na ActionController::Caching::Sweeper class. This class is an Observer that looks for changes to an object via callbacks, and when a change occurs it expires the caches associated with that object in an around or after filter.
Continuing with our Product controller example, we could rewrite it with a sweeper like this:
class StoreSweeper < ActionController::Caching::Sweeper
# This sweeper is going to keep an eye on the Product model
observe Product
# If our sweeper detects that a Product was created call this
def after_create(product)
expire_cache_for(product)
end
# If our sweeper detects that a Product was updated call this
def after_update(product)
expire_cache_for(product)
end
# If our sweeper detects that a Product was deleted call this
def after_destroy(product)
expire_cache_for(product)
end
private
def expire_cache_for(record)
# Expire the list page now that we added a new product
expire_page(:controller => '#{record}', :action => 'list')
# Expire a fragment
expire_fragment(:controller => '#{record}',
:action => 'recent', :action_suffix => 'all_products')
end
end
The sweeper has to be added to the controller that will use it. So, if we wanted to expire the cached content for the list and edit actions when the create action was called, we could do the following:
class ProductsController < ActionController
before_filter :authenticate, :only => [ :edit, :create ]
caches_page :list
caches_action :edit
cache_sweeper :store_sweeper, :only => [ :create ]
def list; end
def create
expire_page :action => :list
expire_action :action => :edit
end
def edit; end
end
source rails guide
So I have a method and corresponding partial for including a set of random photos in the sidebar of certain areas of our site.
Right now I have a random_photos method in ApplicationController set with a before_filter.
That works in the sense that it makes the contents of the random_photos method available wherever I need it, but it also unnecessarily executes some complex SQL queries when I don't know it too (ie, when I don't need to access those random photos).
So, how can I limit the accessing of the random_photos method to only when I really need it?
You can add an :if condition to the before_filter call, like so:
class ApplicationController < ActiveController::Base
before_filter :random_photos, :if => is_it_the_right_time?
Yet another option is to use skip_before_filter. It just depends in how many controllers you want to be different. Use skip_before_filter if there are only a handful of controllers you want to be the exception. Use one of the other suggestions if there are many controllers where you want to bypass the filter.
class ApplicationController < ActiveController::Base
before_filter :random_photos
def random_photos
#photos = Photo.random
end
end
class OtherController < ApplicationController
skip_before_filter :random_photos
...
end
You can keep the random_photos method in ApplicationController, and put the before_filters in your other controllers.
class ApplicationController < ActiveController::Base
...
def random_photos
#photos = Photo.random
end
end
class OtherController < ApplicationController
before_filter :random_photos, :only => 'show'
...
end
It depends on how many functions are making use of random_photos...
If a handful then use vrish88's approach but with an after_filter:
class ApplicationController < ActiveController::Base
after_filter :random_photos, :if => is_it_the_right_time?
...
private
def is_it_the_right_time?
return #get_random_photos
end
end
class SomeController < ApplicationController
def show
#get_random_photos = true
...
end
end
If every function in a controller will make use of it then use the skip_before_filter or move the before_filter in the controller and out of the application controller.
Many ways to get it done, and none is more correct then the next. Just try to keep it as simple and transparent as possible so you don't recreate the functionality months down the road because you forgot where all the pieces are located.