Dear All (especially Ryan, thank you for this great work),
I am just going completely nut with something that probably is just really stupid, but that for the life of me I can't wrap my brain around and solve it.
I am trying to use Can Can, for authorization, and I set it up correctly on everything except (and this is my issue, on a specific thing, that apparently works fine, but that I am not sure if I am doing it right or not - actually I think I am not able to prove that it doesn't work properly).
Let's see if I am able to explain clearly what I am doing:
I have two models (User and Album), that are connected through a third model Share:
(A) User Model
has_many :albums, :dependent => :destroy # ownership
has_many :shares, :foreign_key => "shared_user_id" #, :dependent => :destroy
has_many :shared_albums, :through => :shares
(B) Album Model
belongs_to :user
has_many :shares, :foreign_key => "shared_album_id", :dependent => :destroy
has_many :shared_users, :through => :shares
(C) Share Model
belongs_to :shared_user, :foreign_key => "shared_user_id", :class_name => "User"
belongs_to :shared_album, :foreign_key => "shared_album_id", :class_name => "Album"
Now, in my Ability class, what I would love to do is restricting the actions that can be performed, by a specific user on the (C) Share Model.
In my app, an user can create an album, and after he can add other users to that album (this users are the only one that can access the specific album). In addition, every user that is part of an album,can also add on it new users.
All this is done giving to the users present in the album, the ability to create share (the Share model relate the User and the Album model).
Now the big question, how I restrict the ability to add(create) share to a specific album, only to that users that are part of that album(through share)?
If for example I have:
users => id (1,2,3,4) [a total of 4 users in my app)
albums => id (45,32) [a total of 2 albums in my app)
shares => (album 45 => [user (1,2,3)] ; album 32 => [users(1,4)]) [a total of 5 shares in my app]
How I can say in the Ability class that on album 32(for example), just the user 1 and 4 can add(create) new share(add new user), and that instead the user 2 or 3 they can't?
I already restricted the ability for the user 2 and 3 to anyway access the resource album 32 (I did that on the Album class level), but I want be sure that for whatever reason the ability to create users is restricted also.
What I have until now in my Ability Class is:
def initialize(user)
(A) ALBUM LEVEL
# (1) Every User can create Album, without restrictions
can :create, Album
# (2) Only the user that own the Album can manage it
can :manage, Album, :user_id => user.id
# (3) The Album can be read by all the users that are part of that specific album
can :read, Album, :shares => {:shared_user_id => user.id}
# (4) The Album can be read by every user if the privacy attribute is false
can :read, Album, :privacy_setting => false
(B) SHARE LEVEL
# (1) Only the user that own the Album can manage all the shares for the album
can :manage, Share, :shared_album => {:user_id => user.id}
# (2) The other users (in the album), can just manage themself (their share)
can :manage, Share, :shared_user_id => user.id
# (3) The Share in the album can be read by every user if privacy is false (just read)
can [:read], Share, :shared_album => {:privacy_setting => false}
cannot [:create,:destroy,:delete], Share, :shared_album => {:privacy_setting => false}
#### (X) CRUCIAL POINT CREATE NEW SHARE
can :create, Share, :shared_album => {:shared_users => {:id => user.id}}
end
Is the condition in (X)CRUCIAL POINT the right condition to allow just users that are already part of the album, to add new user to the album???
This is completely driving me insane.
Thank you to everybody, and especially to who will be able to let me understand a bit more of all this.
Best
Dinuz
Your ability.rb seems fine to me.
Your crucial point does the following:
A user can create a share if there is a shared album for that share, and if the the current user is included in that shared album's shared_users.
One thing to keep in mind, though. Since you check the shared_album of a share when authorizing, you must first set it before attempting to authorize.
For example:
#share = #albums.shares.build
authorize :create, #share
The example above will set the shared_album_id of a share, so that you can authorize it.
#share = Share.new
authorize :create, #share
This won't work, since the share does not yet have an album.
Hope this helps.
Related
I have users and companies in a many to many relationship by a join table which has a column for user Role. I'm not sure if this is the best way to have the model set up.
Now each user can have different roles depending on the company, what is the best way to design and access user's role using ActiveRecord associations?
I would like to return via JSON the user's role based on their current company and default to something if their company is nil OR their role has not been set (nil).
Update:
What I've got now after reading Many-to-many relationship with the same model in rails? which is a bit different (many to many on itself).
CompaniesUser
belongs_to :company
belongs_to :user
Company
has_many(:companies_users, :dependent => :destroy)
has_many :users, :through => :companies_users
User
has_one :company
has_many(:companies_users, :dependent => :destroy)
has_many :companies, :through => :companies_users
Appreciate any advice as I'm just starting to learn this!
What you have above is correct, in terms of the ActiveRecord relationships. If you'd like to read more on the subject I believe this is the best source: http://guides.rubyonrails.org/association_basics.html
One problem I see there is that CompaniesUsers should be in singular form: CompanyUser, and then in all cases where you use :companies_users use: :company_users
I am assuming here that the current company of the User is the last one assigned.
Now in order to serialize in JSON format you should add the following in your User ActiveRecord:
def serializable_hash(options = nil)
options ||= {}
h = super(options)
if(defined?self.company_users.last and defined?(self.company_users.last).role)
h[:role] = (self.company_users.last).role
else
h[:role] = 'default_value'
end
end
Imagine a has_many relationship for memberships for clubs:
end
class Club < ActiveRecord::Base
has_many :memberships, :dependent => :destroy
has_many :users, :through => :memberships
validates :name, :is_enrollable, :presence => true
end
class Membership < ActiveRecord::Base
belongs_to :club
end
Assume also that Club has a boolean is_enrollable field in its table. When true, a user can create a Membership associated with that Club. When false, only an admin may create the Membership record.
My question is: how do you set up CanCan's ability.rb to reflect this?
Comment: It's slightly unusual in that a field in the Club table controls the ability to create a Membership record. This cannot work:
can :create, Membership, :club => {:is_enrollable => true}
... since the Membership doesn't exist before its created. Edit: that's not true -- CanCan will work on an unsaved record before authorizing it. See answer below.
(I was planning on withdrawing this question since it's badly posed, but I figured out the answer and thought it might be useful.)
You CAN create a clause in Ability for :create that reads:
can :create, Membership, :club => {:is_enrollable => true}
This works because, in MembershipsController, CanCan will build a new (unsaved) Membership model and and then authorize it, effectively doing:
#membership = Membership.new(params[:membership])
raise <some error> unless Ability.new(user).can?(:create, #membership)
# if we've gotten this far, we can now save the membership
#membership.save
At least, that's what I think is going on.
*Edit:*Last paragraph added to briefly describe the problem set--the data model is complex, but the problem set seems to dictate it. Maybe someone else has a better idea!
I've been working on this problem for more than a month now as I have been learning Ruby on Rails, and I've yet to discover a resolution. I'm using Rails 3.09, Ruby 1.9.2, and PG SQL 8.0. I'm currently unable to upgrade to Rails 3.1 due to gem dependencies.
I have a complex data model for a game that involves collecting rumors about activities of hirelings within the game and displaying the content of rumors to the end user. When a user selects a hireling, they should be able to view all rumors within the database where that hireling is either the actor or the target of the action the rumor is about.
Here is the query in question:
#rumorEvents = current_webuser.player.rumor_events.includes(:rumor_instances =>[:actor, :target]).where(
"(hirelings.id=? AND ( (\"suspectedActor\"=hirelings.name) OR (\"suspectedTarget\"=hirelings.name)))",
session[:selectedHireling]).
paginate(:page => params[:page], :per_page => 1).order('"rumor_instances".updated_at DESC')
As it stands above, this query returns all rumor events where session[:selectedHireling] is an actor, but does not return rumor events where it is a target. If I change
includes(:rumor_instances =>[:actor, :target])
to
includes(:rumor_instances =>[:target])
it will return all events where he is a target, but not an actor.
How can I return all rumor events for both actor and target?
Here are the relevant sections of the models:
class RumorEvent < ActiveRecord::Base
belongs_to :player
has_many :rumor_event_rumor_instance_rels, :dependent => :destroy
has_many :rumor_instances, :through => :rumor_event_rumor_instance_rels
suspectedActor :string(255)
suspectedTarget :string(255)
class RumorInstance < ActiveRecord::Base
belongs_to :player
belongs_to :game_event
has_one :rumor_actor_rel, :dependent => :destroy
has_one :actor, :through => :rumor_actor_rel, :source => :hireling
has_one :rumor_target_rel, :dependent => :destroy
has_one :target, :through => :rumor_target_rel, :source => :hireling
class Hireling < ActiveRecord::Base
# id :integer not null, primary key
# name :string(255)
All rel models are basic, and known working. Here's an example:
# Table name: rumor_event_rumor_instance_rels
#
# id :integer not null, primary key
# rumor_event_id :integer
# rumor_instance_id :integer
# created_at :datetime
# updated_at :datetime
#
class RumorEventRumorInstanceRel < ActiveRecord::Base
belongs_to :rumor_event
belongs_to :rumor_instance
end
Rumors about Game Events verbal description (from designer).
When a game event occurs, 100% accurate and complete information is stored about that act, and kept available for 3 game days. As Hirelings and Players visit the cities near the event's location, they will be able to learn partials of information about the event (rumor instances), which range from "someone was assassinated" to "The Merchant of Venice, Sir Sellsalot was assassinated by Malagant, a hireling that serves (player) Mr. Brown". A player may collect multiple rumor instances about the same event from multiple sources, each with different levels of information, and different levels of accuracy (including in some cases false information).
The code above illustrates the "top level" of information, which acts as a container for all information partials (rumor instances) about a particular event.
I don't have a project with a similar scenario to test on. Off the top of my head, though, includes tags on a relation are cumulative. You should just be able to include 2 include tags:
includes(:rumor_instances => :target).includes(:rumor_instances => :actor)
I think there may be something else. Your data model strikes ne as convoluted. It seems to me that a player should be able to go straight to a rumor instance (which I think is a poor name for what it seems to be representing) through a rumor. What you're doing is complicated enough that I 'smell' something wrong in your data model. Either way, hope this helps.
Consider these two resources: user and group.
Rules:
A group is owned by an user;
A group contains many users;
A user can have many groups;
A user can attend to many groups;
What I have:
class User
has_many :groups, :foreign_key => "owner_id"
has_and_belongs_to_many :attended_groups,
:class_name => "Group",
:join_table => "groups_members",
:foreign_key => "member_id"
end
class Group
belongs_to :owner, :class_name => "User", :foreign_key => "owner_id"
has_and_belongs_to_many :members, :class_name => "User",
:join_table => "groups_members",
:association_foreign_key => "member_id"
end
My question is: what is the best (elegant?) solution to add actions in group controller, and also routes to, while owner sees his group (and all members), let it see who is not there and maybe add it. Something like: /groups/1/add_member/2. Same thing for a user to add a group, while he sees its page.
I've managed to make it work, but I would like to see how it should be. The problem is too simple to have a solution that complicated as mine. Maybe the way I modeled the problem is not the best way too.
Just for the record, I'm a completely newbie to Rails.
Please comment! And thanks in advance!
You probably want to do a nested resource pattern. It adheres to REST though, so you're going to have to deal with the naming conventions for urls.
resources :groups do
resources :users
end
This is a good resource: http://guides.rubyonrails.org/routing.html#resource-routing-the-rails-default
In order to add an existing user to an existing group I would probably define a new route.
match '/groups/:group/users/add' => 'groups#add'
And this in the controller
#group = Group.find params[:group]
#user = User.find params[:user]
#group.users << #user if #user
I'm making a conventional forum to learn/practice Rails. As you're familiar with, Users can create Topics and Posts. Topics and Posts belong to the User that created them, and Posts belong to the Topic they were posted in. Note: Post is a nested resource of Topic.
User Model
has_many :topics
has_many :posts
Topic Model
has_many :posts
belongs_to :user
Post Model
belongs_to :user
belongs_to :topic
So, when a User creates a new Post, the Post needs a user_id and a topic_id.
I know about scoping associations with:
#user.posts.create(:title => "My Topic.")
#topic.posts.create(:content => "My Post.")
But those examples only set user_id and topic_id respectively, not both.
My Question
How can I do something like this:
#topic.posts.create(:content => "This is Dan's post", :user_id => #dan.id)
Without having to expose user_id in the Post model via attr_accessible :user_id?
In other words, I don't want to have to explicitly define :user_id.
I tried things like:
dans_post = #user.posts.new(:content => "the content of my post")
#topic.posts.create(dans_post)
to no avail.
Use build for building associations, instead of new, as it will define the foreign key correctly. To solve your problem, use merge to merge in the user to the parameters for the post:
#topic.posts.build(params[:post].merge(:user => current_user))