Modelling multiple types of users with Devise and Rails 4 - ruby-on-rails

I have 3 types of "users"
Shop
Provider
Customer
The last 2 users will have similar attributes such as
First Name
Last Name
and so on
And the Shop user type will have the most contrast between types.
As far as behaviours they will all be quite different, although Provider and Shop will inherent many of customer behaviours.
It seems the behaviours can be dealt with CanCan as I've researched.
I'm now attempting to how I should authenticate these types.
I have looked at the STI model but I couldn't grasp where I would these extra attributes.
My mental model is as follower:
User is a table and model
The types are abstract models that inherit from this.
So I'm wondering, how do I add attributes such as Business address for just the Shop type?
Or is it that the User Table has a column called Type and that type is associated with these type tables? And within the type tables are the extra attributes?

Don't even bother bringing Devise into this; Devise is for authentication not authorization. Authentication is determining whether or not someone who visits your site is who you think they are, like logging in. Authorization is deciding whether or not a user is allowed to perform some sort of action, like creating a new post.
What you want to do is have some sort of system that assigns a normal user your three different types; CanCan will do something like that. One way to do this on your own is using a permissions number based system. Let's say normal users have permissions level at 100, shop has a level at 50, and provider at 25. Using this system you can determine what actions a user can perform without having to make separate models, which will make your schema unnecessarily complicated. Here's an example of how this would work with say the UserController:
def show
if current_user.permissions == 100
render "customer_show"
elsif current_user.permissions == 50
render "shop_show"
else
render "provider_show"
end
end
The easiest way to do this is to add a column to the user's table called permissions that defaults to say 100 when a new row is created. Here's what that migration would look like:
def change
add_column :users, :permissions, :integer, default: 100
end
As for authenticating, don't worry about it. Let devise do it's thing; every user, no matter what type, will login and sign up in the same way, maybe just having separate forms for each that has a hidden field to set the permissions level for that specific kind of user.

I know I'm late to the party but I'm putting this out for future SO searchers. You CAN authorize actions with Devise. If you have devise models for 'buyer' & 'seller' you can add 'buyer_signed_in?' for whatever action you only want buyers to be able to do. You can also do more specific role-based authorizations as well - check out Devise' page
All in all, Tsiege's solution sounds pretty interesting. Let us know if you had any success with it!

Related

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

Different update / edit methods available to different users

I have a model Post, which is submitted and graded by different Users. The submitter and grader are identified by submitter_id and grader_id in Post model. Note that an user is both a submitter himself and a grader to others.
I want to make sure that the submitter can only edit the content of the Post but not the grade. Likewise, the grader can only edit the grade but not the content.
Is multiple edit methods the way to go? How should I accomplish this otherwise?
You can have a role column in your users table, and the role can be either submitter or grader. Not sure what you are using for authentication, but in case you are using devise, you can access the currently logged in user with current_user helper (in case you are using something else, figure this part out, or add a new helper).
Now in your update method, you can do something like this:
# Controller
# scope post to current user, so that a user cannot edit someone else's post. A crude way to achieve this is post = Post.find(params[:id])
post = current_user.posts.find(params[:id])
post.content = params[:content] if post.submitter?(current_user.id)
post.grade = params[:grade] if post.grader?(current_user.id)
post.save!
# Model - Post.rb
def submitter?(user_id)
self.submitter_id == user_id
end
def grader?(user_id)
self.grader_id == user_id
end
The advantage of keeping those methods in the model is that in case you permission logic changes (who is submitter, or a grader), you need to change it at a single location. DRY.
You can modify the above approach to show error messages, and do other similar stuff. In case you are looking for more granular authorization control, you can look into cancan gem:
https://github.com/ryanb/cancan
Your post model should only be concerned with persisting data. Better to use plain old ruby objects to encapsulate the higher order behavior of grading and submitting. Consider using service objects or form objects.
Each service or form object can then include ActiveModel::Model(rails > v4) to get its own validations.
See more about service and form objects here: http://blog.codeclimate.com/blog/2012/10/17/7-ways-to-decompose-fat-activerecord-models/
If you only have one submit action and one grade action, its probably ok to keep in one controller. But if you start having multiple actions that are related to submitted, and multiple actions that are related to grading, this sounds like they would make great resources controllers on their own.

Rails Security - Enforcing ownership at the model level

I recently coded up a 'friend' capability with my website. The way it works is if a user wants to 'friend' another user, sending a request creates a user_connection record with the original user set at the user_id and the requested user set as the contact_id. If the other user accepts the request, then another user_connection record will be made, with the user_id and contact_id reversed. If both user_connections exist, then the two users are considered friends. This currently gives each user access to any buildings shared between the two users.
I was wondering if there was some way I could enforce my user_connection model to make sure that whoever is creating the record gets set as the user_id. Otherwise it seems that someone could spoof the user_connection create request to make themself a contact of whomever they want, and then could spoof building shares using the same methodology. Are there any built in methods to prevent this?
My first thought was to have an initializer that always set the user_id to the current_user's id, but it appears that current_user is outside of the context of the model.
Don't allow user_id to be provided as a parameter, using strong params.
So, you could create the relation like that:
#friendship = current_user.friendships.new(contact_id: other_user.id)
Also make sure you provide the correct condition for current_user.
That's it... user_id is implied but never provided.

multiple devise authentication realms in a rails application: which is the setting that allows for that?

I've a simple rails application; it has a devise authentication mechanism. Then i've added activeadmin, that brings its devise based authentication mechanism.
There are other question and answers about merging the two models. This question is about which is the setting that makes the two authentication realms distinct.
Example. I perform login in the admin page:
localhost:3000/admin
Here the user model is AdminUser.
then i try to move to a regular (non active-admin) page:
localhost:3000/documents
Here the user model is User.
here, if I test the current_user variable, it is nil and not an instance of AdminUser. That is: the two authentication areas (i used the word realm but I don't know if it is correct) are kept distinct.
I've searched in the activeamdin initializer, but i couldn't find a setting that contains the information of creating a distinct 'authentication realm'.
Update 1 (and possible answer):
They are not distinct.
If i test the current_admin_user, it contains and AdminUser instance.
you have two models User and AdminUser associated with two separate DB tables, right?
do you have separate AA and User model routes inside routes.rb?
finally, you have to set devise settings for User < AR::Base model (AA user model already shipped with activeadmin gem)
in this case, authentications through User and AdminUser models will be separated according to routes you set giving you 'realms' you asked for..
or i didn't get the question...
current_admin_user helper gives you AdminUser instance
current_mega_super_user helper would give you MegaSuperUser instance (by default)

Authorization for actions (not models!) with roles in rails

If I've got a simple rails user model that has an array of roles, is it sufficient enough to control access to actions by simply checking the model's role attribute for that role and blocking/proceeding accordingly?
Is there an advanced system that I ought to leverage due to unforeseen complexity?
Important: I'm not looking to authorize users/roles to models (I am already aware of CanCan). I'm looking to do security at the controller level so that I can break out the functionality in finer detail.
Even more important: Seriously, I'm not necessarily asking about CanCan, please read the question carefully and pay attention! :)
Question 1: YES, Question 2: NO.
I just keep this simple
If you check the models attribute in the controller, the controller will restrict all users that do not have this attribute set.
ex:
def create
#user.find(params[:user_id])
if #user.admin?
#post.new(params[:post])
#post.create!
end
end
make a method in the user model
def admin?
role == "Admin"
end
You should make better code than this. To much logic in the controller, but this will keep all, except admins out.

Resources