Hello my SQL and/or Rails friends.
Let's say we have two models:
class Hostel < ActiveRecord::Base
has_many :beds
end
class Bed < ActiveRecord::Base
belongs_to :hostel
end
When my user, a hostel owner, attempts to create a new booking, the params come in like so:
{bed_id: 12, start_date: "2017-10-13", end_date: "2017-10-15",...}
The bed_id comes from a dropdown menu showing all the current_user's bed names. The id is passed with the rest of the form data. Now, in the BookingsController, I have to manually make sure that a hacker doesn't manipulate the bed_id variable to a bed they don't own.
class BookingsController < ApplicationController
def create
#bed = current_user.beds.where(id: params[:bed_id]).first
if #bed
# create hostel booking
else
# this happens when the user willfully changes the bed_id
# number using DevTools
end
end
I don't mind verifying the user inputs this way, but I was wondering if there's a way to utilize SQL and/or foreign key constraints to make sure that a user doesn't create a booking using a bed that doesn't belong to them?
Here's some pidgin SQL that kind of demonstrates what I'm looking for. Basically, making the database validate that the user_id of the bed_id being used is that of the current_user's ID.
INSERT INTO bookings (start_date, end_date, bed_id, user_id)
VALUES ("2017-10-13", "2017-10-15", 12, 1)
UNLESS (SELECT * FROM beds WHERE id = 12).user_id != current_user.id
# what I'm doing above is verifying that bed #12 has a user_id that
# is the same as the current user's ID. That way, if a user
# manipulates the params, SQL catches it.
EDIT:
Here's a better question:
In Rails, I can open the console and create a new reservation manually:
Booking.new(user_id: 1, bed_id: 12, start: "2017-10-13", end: "2017-10-15")
and the database will create the record, even though the bed with ID #12 does not belong to user #1. Is there anyway to make SQL reinforce these constraints?
I know it is not exactly what was asked, but...
my preferred solution would be to do it in rails, which gives you better control and is platform independent.
(sorry I cannot validate this easily, but it should give you the idea)
class BookingsController < ApplicationController
before_action :check_authorisation, only: :create
def check_authorisation
unless current_user.beds.where(id: params[:bed_id]).first
flash[:danger] = "You are not authorised to access this section"
redirect_to home_url # halts request cycle need to adjust home_url to appropriate
end
end
Related
I'm working on an rails API, more specifically in a create operation.
The workflow that I have is this, I have two rails applications, one is an API and the other is an interface. The API manages the different backend operations in order to handle data, store data, and respond in json format to the interface. The interface serves as the frontend, just making http requests to the API in order to display the information.
In my API I have the 3 model listed below:
class Team < ApplicationRecord
has_many :team_users
has_many :users, through: :team_users
end
class User <ApplicationRecord
has_many :team_users
has_many :teams, through: :team_users
end
class TeamUser < ApplicationRecord
belongs_to :user
belongs_to :team
end
Basically I'm sending an array of user ids from the Interface to the API and I would like to make a query to find out if there's already a team formed by the users (user ids) that I've passed to the API.
I have already tried to do this:
Team.joins(:team_users).where('team_users.user_id' => [3,5])
The problem with this query is that it returns every team that contains the users with id that are equal to 3 or 5. The correct result would be to return a team that has the users 3 and 5 or 5 and 3 as their team members.
Thanks in advance.
Update
The original business rule is this, have an aplication that keeps track of trees, so I have a model named tree and when we create a tree whe must say what team created this tree. So I used a multi select user dropdown field with select2 js library that is how I'm passing the user_ids to the API. So the basic idea is to check is theres already a team composed only by the users passed to the API, if there is already a team I use it's id and say that the tree was registered by that team, if there insn't a team with coposed only buy the users I create a new team and reference it's Id to the tree.
You can approach the problem in different ways. Scrolling over each Team record to check if it contains the associated user_ids is pretty straightforward but inefficient:
team.user_ids.sort == user_ids.sort
But we can make it performant by reversing the process, i.e. iterating over the user_ids to find out corresponding teams, taking their intersection of Team ids and finally checking if any team_id holds those user_ids. This line will return true if there's already a team formed by the users (user ids):
user_ids.map { |id| User.find(id).team_ids }.reduce(:&).present?
You may include it in the User class scope like below:
class User < ApplicationRecord
...
def self.team_exists_for?(user_ids)
# return early
# what should it return when user_ids is empty
# return ??? if user_ids.blank?
# what should it return when any of the id is absent from :users table
# set_of_all_user_ids = Set.new(User.pluck(:id))
# set_of_user_ids = Set.new(user_ids)
# return ??? unless set_of_user_ids.subset? set_of_all_user_ids
# finally
user_ids.map { |id| User.find(id).team_ids }.reduce(:&).present?
end
...
end
Update
So you want to find the team which only has those users provided by user_ids or create a team with them and assign back to the Tree model instance. Combining both approaches described above and defining a scope in the Team model itself seems like a better solution.
class Team < ApplicationRecord
...
def self.find_or_create_for(user_ids)
# find all team_ids which contain user_ids (inclusive)
team_ids = user_ids.map { |id| User.find(id).team_ids }.reduce(:&).flatten
if team_ids.present? # search for the team which ONLY has 'user_ids'
team_id = team_ids.find { |id| Team.find(id).user_ids.sort == user_ids.sort }
end
return Team.find(team_id) if team_id
# or create a team with user_ids and return
team = Team.create! # create a team with required attributes
team.user_ids = user_ids
team
end
...
end
i have implemented this as
add a field key: string on Team and in Team model
class Team < ApplicationRecord
has_many :team_users
has_many :users, through: :team_users
#callback
before_validation :update_key
def update_key
self.key = Team.key_for_users(self.users)
end
def self.key_for_users(users)
users.sort.map(&:id).join("_")
end
end
so basically after this callback whenever you will create a team there will be a key
for example: -
users = [3,5]
then key in Team will be 3_5
or users = [5,3]
then key in Team will be 5_3
From this we can easily get the result what you wanted
example: -
user_ids = [3,5]
[14] pry(main)> user_ids_simple = user_ids.join('_')
=> "3_5"
[15] pry(main)> user_ids_reverse = user_ids.reverse.join('_')
=> "5_3"
and query will be like this: -
Team.where("key IN (?)",[user_ids_simple, user_ids_reverse])
it may be helpful for you. thanks
User has_many constructusers, the latter being a join table for a has_many :through relationship to Construct. For the application purposes, the boolean roles are defined in the join table (constructusers.manager, constructusers.operator, etc.), while admin is a user attribute.
So when it comes time to define the policies on the actions the following throws a no method error for 'manager', while a relationship is recognised ActiveRecord::Relation:0x000001035c4370
def show?
user.admin? or user.constructusers.manager?
end
if the relationship (I assume the proper one) is correct, why is there no recognition of the boolean attribute?
As per comment below, for the simple reason that is plural. Thus filtering requires:
Constructuser.where(['construct_id = ? and user_id = ?', params[:id], current_user]).first
...which is running in the controller and impacts the view. Nonetheless, for proper Pundit handling, this needs to be factored out... still de application_controller in a before filter to set that attribute. However a before_filter :set_constructuser_manager with that find condition, with nil case handling, still has no impact when stating the policy
def show?
set_constructuser_manager?
end
Update: as per comment below. Pundit class private method
def contractorconstruct
#contructs = Construct.where(['constructusers.user_id = ?', user]).joins(:users).all
#contractorconstruct ||= Contractor.where(['construct_id IN (?)', #contructs]).first
end
and action rule
|| contractorconstruct?
returns no method error.
manager? will be a method on an instance of Constructuser, not on the relation. Think about what you are asking, "Is this constructusers a manager?" - it makes no sense. How would the computer know what constructuser you are talking about?
If a user has_many constructusers, in order to use manager? you need to find the instance you are concerned about. If this is in the ConstructPolicy, then you need to find the specific constructuser that links user to the construct that you are authorizing, then check if that single constructuser is manager?.
If you are in the Construct controller, you'll have something like
class ConstructsController
before_action :set_construct
def show
authorize #construct
# ...
end
# ...
end
In your policy then, user will be the current user and record will be #construct.
class ConstructPolicy
def show?
user.admin? || constructuser.manage?
end
private
def constructuser
#constructuser ||= Constructuser.find_by(user_id: user, construct_id: record)
end
end
In my omniauth & omniauth-identity based app I came across the follwoing issue:
There are three tables that manage Userdata:
# Stores User data _not_ authentication data
class User
include Mongoid::Document
has_many :identities
field :email, type: String
end
# Stores Authentication data (i.e. Facebook)
class Identity
include Mongoid::Document
belongs_to :user
end
# Stores Authentication data for omniauth-identity
class LocalIdentity < Identity
include OmniAuth::Identity::Models::Mongoid
field :email, type: String
end
The issue is now that I have duplicated the email-field in LocalIdentity. So whenever a user changes his email-address in the User model I need to synchronize the email address. This seems trivial at first but can get real messy as soon as there are more fields involved etc.
So my question is: Is there a way to remove the redundance between User and LocalIdentity?
While pondering about a solution i came to the following conclusions:
Multi Inheritance isn't supported so LocalIdentity < Identity, User won't work
Having a LocalIdentity embedded in every User and
write-trough the values won't work b/c validations wouldn't work
Requirement:
I'm creating a twitter like web application for the local campus where users can share their feedback over a service within 140 characters just like the way twitter does.
For every feedback , there can be multiple responses to the feedback. These responses are similar to twitter replies , where replies are also 140 character response like a tweet.
Essentially the screen for entering a feedback or a response is the same with the same functional elements. Example would be :-
This is a demo feedback - user 1
This is a response to the feedback - user 2
I'm using rails for the development and this is what my feedback model looks like
class Feedback < ActiveRecord::Base
attr_accessible :title
end
My table structure in the database looks like this:-
Feedback Table
id , integer , primary key title, varchar (255)
This is how I want to manage responses to a feedback
A separate table named "Feedback_Responses" with the following structure :
feedback_id (int) [This is a parent feedback id] response_id
(int) [This is a response to the parent feedback id]
My Question
I understand that self referential mapping is needed over here with many to one relationship. However, I'm not clear as to how to proceed with creation of a response from the same view that is used to create a feedback. And secondly, a response is another feedback to the system so unless a response is created and its id is generated , the Feedback_Responses table wont be populated with the required mapping. So, in this case I'm really clueless and perhaps confused as to how to create responses and manage their mappings on a separate table. I'm starting to learn rails with this application.
I'm not understanding your design very well, but it looks like your main entity (equivalent to a tweet) is the "feedback" and each "feedback" can have many "responses" (like comments). What is not clear to me is whether the "response" can be a "feedback" in its own right, but I'm going to assume it's not, for now. So, your design says, according to this description, that a feedback has many responses. I find that "feedback_responses" table irrelevant for the matter, because it sounds like a response is connected to one and only one feedback. So, drop the table.
So, I would write it like this:
# table feedbacks
# id: Integer
# title: Varchar 140
class Feedback < ActiveRecord::Base
has_many :responses
end
# table responses
# id: Integer
# feedback_id: Integer
class Response < ActiveRecord::Base
belongs_to :feedback
end
Still, if you want to handle the response has a feedback which is not a response to another feedback, you can just use inheritance as a solution:
# additionally feedback_id: Integer to the feebacks table
class Feedback < ActiveRecord::Base
has_many :responses
end
class Response < Feedback
belongs_to :feedback
end
View generation will be automatically easy as long as you have two distinguishable models.
class Feedback < ActiveRecord::Base
attr_accessible :title
validates :title, :length => { :maximum => 140 }
has_many_and_belong_to :responses
end
class Response < ActiveRecord::Base
has_many_and_belong_to :feedbacks
end
To fetch responses from a feedback object use #feedback.responses
same for the response object #response.feedbacks
This relation is easy to maintain. You can add middle table if you need.
I've started a new project which requires accounts to be able to view their candidates and related tasks.
account.rb
has_many :candidates
candidate.rb
has_many :tasks
belongs_to :account
task.rb
belongs_to :candidate
Routes are set up to allow /candidates/4/tasks/3 where account X has access to candidate 4 which has task 3.
tasks_controller.rb is currently like:
def show
#task = Task.find params[:id]
end
QUESTION: What is the best practice approach to ensure that other accounts don't have access to task 3?
One idea might be something like this but seems very messy:
def show
#task = Account.find(current_account).candidates.find(params[:candidate_id]).tasks.find(params[:id)
end
So if the join fails, you don't have access.
Another way might be done using scopes. Where you make sure all tasks queried are joined with candidates and current_account.
I could also do a before_filter to do a standalone query on candidates table to check that the account has access. This will add an extra query so not ideal.
I'm waffling here... but would love to know how others go about this?
Do it by rails way...
current_account = Account.find params[:id]
#tasks = current_account.candidates.collect(&:tasks)
This will do your job.