Apply validation module to model in certain controllers only - ruby-on-rails

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).

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

Devise invitable and validating nested attributes

I have an app where users can invite other users, and I have added a has_many model Enrollment model that has several attributes.
The devise_invitable module seems to ignore validations, so in my case, if my nested attributes fail validation, it is never caught, and the user still gets invited.
I found the validate_on_invite (defaults to false) setting, which may work. But then, I would need to add conditional validations to the other User attributes (name, address, etc.) so that they do not trigger during the invitation process.
I have a custom create controller method right now, and can add some logic to check the presence of parameters, etc. However, is there an easier way to do this that relies on the model validations?
UPDATE
I tried changing validate_on_invite to true and got nested attributes and validations working. The problem I have now is that in the User model you can't (as far as I can tell) do a conditional validation based on the controller action (i.e. the invitations controller with the create action).
I think I am back to some custom code in the invitations controller.
I ended up stumbling upon if: :accepted_or_not_invited?. I added this to all the User validations (other than email address). I can invite users with nested enrollment attributes that are automatically saved. I don't have the nested validations working yet but for not I added a monkey patch in my invitations controller to validate the enrollment params.

Rails validation vs authorization

I have a scenario where I am unsure of whether a particular function should be considered validation or authorization. I can code it either way.
Users can "like" articles.
When a user creates a new "like" I need to ensure the user has not already liked the article. The front end will limit the functionality however I want backed end safeguards.
Should the process of ensuring a user has not already liked the article be considered validation or authorization?
Further to comments received:
If auth determines if the option is available to the user, or not & validation determines if the user selection is valid then...
Auth will make the option to click "like" available even when then user has previously "liked" and therefore it will inevitably fail validation.
This thinking results in an invalid option being presented to the user.
Is ensuring the user can only delete/edit their own "likes" auth or validation? The previous logic implies it should be validation as the user is either authorised to add/update or destroy within the model or not and ensuring their actions are valid is the role of validation However it would be illogical to present the option to delete another user's like only to reject upon failed validation.
This is validation. I don't know your model architecture, but if you have a Like model, you could validate like this:
class Like < ActiveRecord::Base
belongs_to :user_id
belongs_to :article_id
validates :article_id, uniqueness: { scope: :user_id }
end
You should also make sure that a unique constraint is present at the DB level, to avoid a potential race condition.
This sounds more like validation. You have to check in your model that this article was liked by this user or not. If it is, then this like is invalid and he can't like it now. Otherwise, it will pass the validation and the user will be able to like this article.
Authorization should come, when some user can like some set of articles, but not all, in those situation, In my honest opinion.
a. should use rails validation to make sure he/she can like once not more then that.
b.authorization is to restrict user from hitting like.
authorization would be: is this user allowed to perform this action; validation: will this action succeed. given that user is allowed to 'like', ensuring he can do it only once is a validation problem. to solve it put unique constraint on db level (user_id, article_id).

Setting Pundit role for user from Devise Registrations New View / Controller

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.

ruby on rails way to destroy extra session variables in a model?

I have additional variables I add to an authlogic users session like:
session[:current_profile] = extra_id
I currently destroy these on logout in a controller like:
session[:current_profile] = nil
I'd like to clean this up and destroy them in the session model in the after_destroy method like:
def after_destroy
session[:current_profile] = nil
end
This session method doesn't seem to be callable from the models though. Any idea how to destroy a session variable from a model?
Thanks!
You're really not supposed to alter things in the Controller space from the Model space, which is to say, a Model should not be controlling a Controller. Models should be able to run independently of a controller, such as in unit tests where no controller is present.
While you might be able to wrangle this sort of thing with an Observer, I don't know of an easy way to do this. It's probably better to have the controller perform all the required actions directly.
If you put an after_destroy hook like this in you will have some serious side-effects if, for example, a user logged in as an admin destroys another user account and then their session profile suddenly disappears.

Resources