How to handle before filter for specific action in Grape?
Asked Answered
P

3

11

I'm mounting Grape in my Rails project to build a RESTful API.

Now some end-points have actions need authentication and others which don't need authentication.

As for example I have users end-point which looks something like:

module Backend
  module V1
    class Users < Grape::API
      include Backend::V1::Defaults

      before { authenticate! }

      resource :users do

        desc "Return a user"
        params do
          requires :id, type: Integer, desc: 'User id'
        end
        get ':id' do
          UsersService::Fetch.new(current_user,params).call
        end

        desc "Update a user"
        params do
          requires :id, type: Integer, desc: 'User id'
          requires :display_name, type: String, desc: 'Display name'
          requires :email, type: String, desc: 'Email'
        end
        post ':id' do
          UsersService::Save.new(current_user,params).call
        end

        desc "Reset user password"
        params do
          requires :old_password, type: String, desc: 'old password'
          requires :password, type: String, desc: 'new password'
        end
        post 'password/reset' do
          PasswordService::Reset.new(current_user,params).call
        end

        desc "Forget password"
        params do
          requires :email, type: String
        end
        post 'password/forget' do
          PasswordService::Forget.new(current_user,params).call
        end            

      end
    end
  end
end

Now as you can see, all the actions except password/forget needs the user to be logged-in/authenticated. It doesn't make sense too to create a new end-point let's say passwords and just remove password/forget there as logically speaking, this end-point should be related to users resource.

The problem is with Grape before filter has no options like except, only in which I can say apply the filter for certain actions.

How do you usually handle such a case in a clean way?

Platform answered 17/10, 2016 at 14:8 Comment(0)
P
3

A dirty way to help would be by using namespace, something like:

module Backend
  module V1
    class Users < Grape::API
      include Backend::V1::Defaults

      namespace :users do
        desc "Forget password"
        params do
          requires :email, type: String
        end
        post 'password/forget' do
          PasswordService::Forget.new(current_user,params).call
        end

        namespace do
          before { authenticate! }

          desc "Return a user"
          params do
            requires :id, type: Integer, desc: 'User id'
          end
          get ':id' do
            UsersService::Fetch.new(current_user,params).call
          end

          desc "Update a user"
          params do
            requires :id, type: Integer, desc: 'User id'
            requires :display_name, type: String, desc: 'Display name'
            requires :email, type: String, desc: 'Email'
          end
          post ':id' do
            UsersService::Save.new(current_user,params).call
          end

          desc "Reset user password"
          params do
            requires :old_password, type: String, desc: 'old password'
            requires :password, type: String, desc: 'new password'
          end
          post 'password/reset' do
            PasswordService::Reset.new(current_user,params).call
          end            

        end
      end
    end
  end
end

This way we wont run before filter for users/password/forget but for the rest we will run before { authenticate! }

Platform answered 19/10, 2016 at 15:16 Comment(2)
upvoted.. I wonder if now there's an more elegant way of doing itUnbound
Works only directly in endpoint class. It is not working if you would like to do something like before callback for specific mount API. For example I want to run it got mount Admin::Base but I don't want it to be run on Public::BaseCrosslegged
C
13

One way I could think of is to use route_setting to add custom attributes for the routes you would want to by-pass auth for. Check for these attributes in the before filter before calling authenticate!. Something like the below should work:

module Backend
  module V1
    class Users < Grape::API
      include Backend::V1::Defaults

      before { authenticate! unless route.settings[:auth] && route.settings[:auth][:disabled] }

      resource :users do

        desc "Return a user"
        params do
          requires :id, type: Integer, desc: 'User id'
        end
        get ':id' do
          UsersService::Fetch.new(current_user,params).call
        end

        desc "Update a user"
        params do
          requires :id, type: Integer, desc: 'User id'
          requires :display_name, type: String, desc: 'Display name'
          requires :email, type: String, desc: 'Email'
        end
        post ':id' do
          UsersService::Save.new(current_user,params).call
        end

        desc "Reset user password"
        params do
          requires :old_password, type: String, desc: 'old password'
          requires :password, type: String, desc: 'new password'
        end
        post 'password/reset' do
          PasswordService::Reset.new(current_user,params).call
        end

        desc "Forget password"
        route_setting :auth, disabled: true
        params do
          requires :email, type: String
        end
        post 'password/forget' do
          PasswordService::Forget.new(current_user,params).call
        end            

      end
    end
  end
end
Chronologist answered 22/8, 2017 at 9:55 Comment(2)
I like this better than the accepted answer. The namespace approach means that the before action has to live inside the nested namespace. This would allow registering it in the api base and only excluding it on a select few endpoints, potentially in different files.Moskva
before { authenticate! unless route.settings.dig(:auth, :disabled) } in that way is a little bit shorter :)Crosslegged
P
3

A dirty way to help would be by using namespace, something like:

module Backend
  module V1
    class Users < Grape::API
      include Backend::V1::Defaults

      namespace :users do
        desc "Forget password"
        params do
          requires :email, type: String
        end
        post 'password/forget' do
          PasswordService::Forget.new(current_user,params).call
        end

        namespace do
          before { authenticate! }

          desc "Return a user"
          params do
            requires :id, type: Integer, desc: 'User id'
          end
          get ':id' do
            UsersService::Fetch.new(current_user,params).call
          end

          desc "Update a user"
          params do
            requires :id, type: Integer, desc: 'User id'
            requires :display_name, type: String, desc: 'Display name'
            requires :email, type: String, desc: 'Email'
          end
          post ':id' do
            UsersService::Save.new(current_user,params).call
          end

          desc "Reset user password"
          params do
            requires :old_password, type: String, desc: 'old password'
            requires :password, type: String, desc: 'new password'
          end
          post 'password/reset' do
            PasswordService::Reset.new(current_user,params).call
          end            

        end
      end
    end
  end
end

This way we wont run before filter for users/password/forget but for the rest we will run before { authenticate! }

Platform answered 19/10, 2016 at 15:16 Comment(2)
upvoted.. I wonder if now there's an more elegant way of doing itUnbound
Works only directly in endpoint class. It is not working if you would like to do something like before callback for specific mount API. For example I want to run it got mount Admin::Base but I don't want it to be run on Public::BaseCrosslegged
P
0

A little bit more clear but maybe not so obvious way will be splitting your namespace into separate subclasses:

module API
  module Users
    class Root < Grape::API
      namespace :users do
        mount Create # <= `before` callback is not executed

        before do
          authenticate!
        end

        mount Update # <= `before` callback is executed
      end
    end
  end
end

The relative directory structure thus will look something like this:

 api/users/root.rb
 api/users/create.rb
 api/users/update.rb
Punctate answered 21/10, 2019 at 15:8 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.