# Public: mixes in CRUD controller actions to your Action Controller # classes to provide a simple RESTful actions that provides # resource-based permissions with built-in integrations with # Doorkeeper scopes. # # Examples # # class PostsController < ApiController # include Garage::RestfulActions # # def require_resources # @resources = Post.all # end # # def require_resource # @resource = Post.find(params[:id]) # end # end module Garage module RestfulActions extend ActiveSupport::Concern included do before_action :require_resource, :only => [:show, :update, :destroy] before_action :require_resources, :only => [:index, :create] before_action :require_action_permission_crud, :only => [:index, :create, :show, :update, :destroy], :if => -> (_) { verify_permission? } end module ClassMethods def resource_class=(klass) @resource_class = klass end def resource_class @resource_class ||= default_resource_class end private def default_resource_class class_name = name.sub(/Controller\z/, '').demodulize.singularize begin return "#{class_name}Resource".constantize rescue NameError begin return class_name.constantize rescue NameError raise "Garage needs `#{class_name}Resource` or `#{class_name}` for resource class of #{name} but neither was found. If you want use an alternative class for the resource class, specify the resource class by `.resource_class=` in your controller." end end end end # Public: List resources # Renders `@resources` with options specified with `respond_with_resources_options` # Requires `:read` permission on `resource_class` specified for `@resources` def index respond_with @resources, respond_with_resources_options end # Public: Get the resource # Renders `@resource` with options specified with `respond_with_resource_options` # Requires `:read` permission on `@resource` def show respond_with @resource, respond_with_resource_options end # Public: Create a new resource # Calls `create_resource` in your controller to create a new resource # Requires `:write` permission on `resource_class` specified for `@resources` def create @resource = create_resource respond_with @resource, :location => location end # Public: Update the resource # Calls `update_resource` in your controller to update `@resource` # Requires `:write` permission on `@resource` def update @resource = update_resource respond_with @resource, respond_with_resource_options end # Public: Delete the resource # Calls `destroy_resource` in your controller to destroy `@resource` # Requires `:write` permission on `@resource` def destroy @resource = destroy_resource respond_with @resource, respond_with_resource_options end private # Private: returns either `:read` or `:write`, depending on the current action name def current_operation if %w[create update destroy].include?(action_name) :write else :read end end # Private: Call this method to require additional permission on # extra resource your controller handles. It will check if the # current request user has permission to perform the operation # (`:read` or `:write`) on the resource. # # Examples # # before_action :require_recipe # def require_recipe # @recipe = Recipe.find(params[:recipe_id]) # require_permission! @recipe, :read # end def require_permission!(resource, operation = nil) operation ||= current_operation resource.authorize!(current_resource_owner, operation) end # Private: Call this method to require additional access on extra # resource class your controller needs access to. It will check if # the current request token has an access permission (scope) to # perform the operation (`:read` or `:write`) on the resource # class. # # Examples # # before_action :require_stream # def require_stream # require_access! PostStream, :read # end def require_access!(resource, operation = nil) operation ||= current_operation ability_from_token.access!(resource.resource_class, operation) end # Private: Call this method to require additional access and # permission on extra resource your controller performs operation # on. def require_access_and_permission!(resource, operation = nil) require_permission!(resource, operation) require_access!(resource, operation) end def require_action_permission_crud if operated_resource require_access_and_permission!(operated_resource, current_operation) else Rails.logger.debug "skipping permissions check since there's no @resource(s) set" end end alias :require_action_permission :require_action_permission_crud # Private: Call this method if you need to *change* the target # resource to provision access and permission. # # def require_resources # @resources = Post.where(user_id: @user.id) # end # # By default, in `index` and `create` actions, Garage will check # `:read` and `:write` access respectively on the default # `resource_class` of `@resources`, in this case Post class. If # you need more fine grained control than that, you should specify # the optional parameters here, such as: # # def require_resources # @resources = Post.where(user_id: @user.id) # protect_source_as PrivatePost, user: @user # end # # This way, the token should require access scope to `PrivatePost` # (instead of `Post`), and the authorized user should have a # permission to operate the action on resources owned by `@user` # (instead of public). The `:user` option will be passed as # parameters to `build_permissions` class method. def protect_resource_as(klass, args = {}) if klass.is_a?(Hash) klass, args = self.class.resource_class, klass end @operated_resource = MetaResource.new(klass, args) end def operated_resource if @operated_resource @operated_resource elsif @resources MetaResource.new(self.class.resource_class) else Garage.configuration.cast_resource.call(@resource) end end # Override to set @resource def require_resource raise NotImplementedError, "#{self.class}#require_resource is not implemented" end # Override to set @resources def require_resources raise NotImplementedError, "#{self.class}#require_resources is not implemented" end # Override to create a new resource def create_resource raise NotImplementedError, "#{self.class}#create_resource is not implemented" end # Override to update @resource def update_resource raise NotImplementedError, "#{self.class}#update_resource is not implemented" end # Override to destroy @resource def destroy_resource raise NotImplementedError, "#{self.class}#destroy_resource is not implemented" end # Override this if you want to pass options to respond_with in index action def respond_with_resources_options {} end # Override this if you want to pass options to respond_with in show, update and destroy actions def respond_with_resource_options {} end def location { action: :show, id: @resource.id } if @resource.try(:respond_to?, :id) end end end