Setting Pundit role for user from Devise Registrations New View / Controller - ruby-on-rails

I have both Pundit and Devise setup and working correctly in my rails app. However I am unsure about how to let the user decide their role when signing up.
At the moment:
I have a URL param which is passed to the Devise new view.
In the form_for I set a hidden field called role to the value of the param.
This works.
But I am concerned that a malicious user could change this param to say "Admin" and now they are an admin.
How should I handle this? I don't want to put a restriction in the model as that will cause issues when I want to create an admin. Should I override the devise registrations controller to put a check in there?

You don't need to override Devise's RegistrationsController for what you're trying to do.
If you want admins to be able to create users that have an arbitrary role set, you could simply use your own controller. Devise still makes it easy to create a user yourself, so you'll just have to make a controller handling this. Of course, don't forget to protect it using Pundit so only admins can use this functionality.
This approach still works if you use the Confirmable module. As no confirmation e-mail will be sent on user creation, though, you'll either have to call user.confirm! after saving the model to immediately unlock the account, or manually send the confirmation e-mail using user.send_confirmation_instructions.
Edit:
This Pundit policy may or may not work for what you're trying to do. You will have to override the create action of Devise's RegistrationsController here in order to use Pundit's authorize method. For dryness' sake, you should also move the roles list elsewhere, perhaps into the model.
class UserPolicy < Struct.new(:current_user, :target_user)
def create?
registration_roles.include?(target_user.role) if current_user.nil?
end
private
def registration_roles
%w(RED BLU Spectator)
end
end

After a fair amount of googling I have an answer. First stick some validation in your model for the roles Active Record Validations Guide: See 2.6 inclusion: validator option
After this your roles are validated to ensure they are correct, you could of course have a lookup table as well. Then you have two options:
Use a conditional before_save Callback for new records. Then check if the new record has the role your protecting and if so raise an error. To catch later (in an overridden devise controller (see second option).
Override the Devise registrations controller See this SO question. And put some checks in a completely overridden create action. Use the session to store the url param passed to the new action (also needs to be completely overridden). Then if the create action fails and redirects to new you still have access to the role in the session (as the param will be cleared from the URL unless you manipulate it).
So either way you need to override the registrations controller, its just a case of how much.
I suspect there is a way to do this with just Pundit. But I have yet to be able to get it to work.

Related

Rails, can validate a user crating a record is admin, from the model?

I am attempting to make a new record object invalid if the user_id of the object is associated with an account that is not admin.
In the model I'd like to make a validation checks if the object to be created user_id.admin == true.
Neither of these solutions work:
validates :user_id, User.find(:user_id).admin?
before_save :user_is_admin, User.find(self.user_id).admin?
So, my question is, how do I write a validation that looks up the user and checks if they are an admin, and throws an error if they are not?
P.S. I am already doing admin checking in the controller as a before_action, but I'd like to invalidate the object if a non admin user manages to create one somehow...and for testing purposes.
If this isn't a best practice I'd still like to know a bit more about creating validity checks in rails.
I generally wouldn't encourage doing this type of authorization at the model/database level. Rather I would suggest that you abstract your authorization code into it's own layer and handle this at the controller level, then rely on tests to verify that nobody can create an object except through the controller.
https://github.com/elabs/pundit is a great gem for integrating an authorization layer into your Rails application.
If you still did want to do this validation at the model level, you could do something like this:
validate :creator_is_admin
def creator_is_admin
errors[:base] << I18n.t('object_class.activerecord.validations.admin_create_check_failure') unless User.find(user_id).try(:admin?)
end
I mostly validating before action goes to the Model, controller could be good for it.
###Post Model Sample
validate :is_admin?
def is_admin?
unless User.find_by_id(user_id).admin
errors.add(:not_admin, "The post not belongs to you || not admin :) ")
end
end

How can I allow only a specific user role to update an attribute to a specific value

Afternoon, got a bit of an issue I am not sure how to resolve.
I am trying to setup some rules that allows only certain types of user roles to update the status attribute on a model to a certain status.
So I looked into doing this with pundit as it seems to be an authorisation issue, however one problem with that is you cannot pass the params to the pundit policy which I would need access too (so I can see what attribute they are trying to change to), and it seems that its bad practise to pass params to a pundit policy.
The next option was to make it a callback in the model, however the problem here is I don’t have access to the current_user inside the callback and again it seems its bad practise to add the current_user helper into a model.
So I am left with perhaps doing it in the controller? Again does not seem the right place for it?
An example to make it a little easier to understand:
I want to allow a User with the role of admin to be allowed to change the status of a post to "resolved", no one else is allowed to change the status to "resolved"
Try this,
create a instance method in User model like bellow,
def is_admin?
self.has_role(:admin) # if you are using rolify
---OR---
self.status == "admin" # if you have status attribute in your user table
end
Then call this method on current_user in edit/update method of post controller. to check current_user is admin or not

Apply validation module to model in certain controllers only

I have a model that can be edited by two different types of users. The first has a login and has special privileges (let's call them a 'user'). The second is just some random user without a login with limited privileges (let's call them a 'guest').
The guest only really interacts with the model through one controller and we want certain validations to only apply in this case. The validations we want to apply exist within a module.
I tried doing something like this in the controller action, but it didn't seem to work:
#object = Model.find(params[:object_id])
#object.extend SpecialValidations
Then we would check for the objects validity (maybe directly or when updating attributes) and then display any errors generated by the validations.
Is there a better way to do this?
Thanks!
One alternative is to include the following in your Model:
attr_accessor :guest
def run_special_validations?
guest
end
validate :special_validation1, if: run_special_validations?
validate :special_validation2, if: run_special_validations?
Then, by having the controller set #object.guest = true, you will tell the object to run the conditional validations.
You could keep the validation without any conditions, and just skip it in the user controller (by using the update_attribute method, for example).

rails devise hook into on_login

I'm looking to hook into devise after login / after session create. How would I go about doing this?
Basically I want to set a users location every time they login, and to do that I need an after login hook of sorts.
Devise is built on Warden, so you can use Warden's after_authentication hook.
Put this in an initializer:
Warden::Manager.after_authentication do |user,auth,opts|
# do something with user
end
The remote IP address and other request info is stored in auth.request (i.e. auth.request.remote_ip).
See https://github.com/hassox/warden/wiki/callbacks
Devise updates the value of the user.current_sign_in_at timestamp on successful login. So, you could simply add a before_save filter to your User model. In that filter, check to see if the value of this field has changed, and if it has, set the users location.
BTW - I'm not sure what you mean by "location" - if you mean IP address, Devise already stores that for you.
Here's a page from the devise wiki: How To: Redirect to a specific page on successful sign in.
In summary, the recommendation is to add the following method to the application controller:
app/controllers/application_controller.rb
def after_sign_in_path_for(resource)
custom_location_for(resource) || welcome_path
end
In the above code, resource means the object (user, account, etc) that you've implemented devise authentication for. (The object that has the devise_for in your routes.)

Authlogic and Single Access Token

I am having a hard time finding a simple tutorial on how to enable single access token authentication using authlogic. There is some documentation but it isn't very helpful.
I added single_access_token to my db, I added this:
single_access_allowed_request_types :any
to my Session class. but I still don't understand how a user is authenticated using the credentials param that is passed over every call. My require_authentication before filter does a standard check for current_user like this:
def current_session
return #current_session if #current_session
#current_session = Session.find
end
def current_user
#current_user = current_session && current_session.record
end
But is that enough to work? Does the Session.find method do the magic to log the user is based on my params or do I have to create separate method that actually check if the user_credentials param is there and then find the user based on it and then log that user in. I am confused if I really am "creating" a new session everytime I use a SAT or if I'm just setting current user in a before filter every time an API call is made.
Any help would be amazing! Thanks!
I implemented a single_access_token solution with authlogic and what I had to do was add single_access_allowed_request_types :all to the UserSession model.
Then I added the following to the controller where I wanted to allow single_access_token authentication.
def single_access_allowed?
["some_action_1","some_action_2","some_action_3"].include?(action_name)
end
It looks like you're missing the controller code. So if you had two actions "get_user_info" and "update_user_info" you would add.
def single_access_allowed?
["get_user_info","update_user_info"].include?(action_name)
end
The only thing I had to do make this work was
add a field called single_access_token to my users-table
add a method called single_access_allowed? to each controller where single access should be allowed.
This method would look like this:
# method for authlogic: defines for which action the single-access-token can be used
def single_access_allowed?
(action_name == "deliver") || (action_name == "delivery_status")
end
I did not have to add anything in UserSessionsController or the UserSession object. Authlogic handles that for you. With a single-access-token only one request is authenticated, so there is not a persistent session. Each request has to send the single-access-token. Hence the name: a token to get a single access :)
Hope this helps.
The source code of authlogic is the best documentation on the single access token. This is the specific section that discusses it.
You will need to add a private method called single_access_allowed? in the controller where you are trying to let users access. The Single Access Token is passed by default as a URL encoded parameter using the name user_credentials. So to hit your controller without logging in it will be /your_route/?user_credentials=xxxxxx

Resources